Skip to main content
All articles

Can We Send the Signature Once the Save Call Returns?

· 10 min read
Bankware Global Engineering

As the article on observing consensus progress explains, a server responding to requests is different from a server finalizing transactions. Once we can observe where consensus is waiting, the next question is when that wait may safely end. If it is waiting for storage, moving on sooner is not always an improvement.

A consensus node sends other nodes signed messages supporting a block. Those nodes treat the signatures as votes. But suppose the sender abruptly stops and then restarts. What if it is the only node that cannot remember the vote it already sent?

To prevent this, nigo-protocol first records consensus actions in local storage before broadcasting them. The question was what “the save call returned successfully” actually included. In the implementation and verification work of September 2026, the key was to distinguish a write request, a database commit, and completion of the storage synchronization request, rather than treating all three as a single success.

When Other Nodes Remember but I Do Not​

Consider a small example. Node A has voted for candidate X in the second round of deciding the block at height 10. The height is the block's position in the chain; a round is another attempt to reach consensus at the same height. For this example, assume a node must not support different candidates in the same voting phase at the same height and round.

If A broadcasts its vote before saving that action, a gap opens between the two steps. Other nodes may have received A's signature for X while A's own record still exists only in memory. Suppose A stops at that point. After restarting, it receives candidate Y with no record of having supported X. If it is allowed to vote again without knowing its previous action, other nodes will see A supporting both X and Y.

This is an illustrative failure model explaining why the order matters. It is not a report of double voting observed in this verification. The important point is that a local failure cannot retract a message already delivered elsewhere. Deciding that “the write failed, so the vote never happened” does not erase the signature held by its recipients.

We therefore reverse the order. First leave a record that can be recovered after a restart, then broadcast the vote. If the node stops after saving but before broadcasting, it can use the surviving record to judge its next action. It is safer to remember an action the network has not yet seen than to forget an action the network has already seen.

This record serves a different purpose from the transactions in a block. It lets a particular validator recover its own consensus actions and the consensus evidence it has already obtained. In nigo-protocol, these are the validator's safety records, or safety WAL. WAL means a write-ahead log: a record written before the action that depends on it.

Finishing a Write Request Is Not the Same as Committing It​

Calling the save function first does not automatically guarantee this order. If the function joins a database transaction started by its caller, it may return while the actual commit is still waiting for the outer operation to finish. A commit is the step that confirms the transaction's changes in the database.

Suppose A saves its safety record and immediately sends its vote. The visible call order looks correct. But if the record is still part of an outer transaction, and that transaction later fails and rolls back, the vote may have been broadcast while its record disappears. The mistake was interpreting the save call's return as confirmation that the commit had finished.

For that reason, nigo-protocol's relational storage path does not let safety records join an unfinished outer transaction. It rejects such calls and requires the safety record's own transaction to commit. This rule does not apply to every write inside the node. Ledger data that must be committed or rolled back together, such as blocks and finality evidence, retains its existing atomic storage relationship.

A further question remains after commit. Can an acknowledgment that the database committed the write be treated as confirmation that the synchronization required by this storage path has also finished? Because this depends on the storage implementation and configuration, that confirmation became an explicit part of the safety record's success contract.

A simplified normal path for safety records in relational storage. No step proceeds without evidence that the preceding step has finished. The signature itself may be generated before the write; the boundary being protected here is external broadcast.

That last distinction matters. The current implementation also creates the signed message before storing it. The guarantee is not “no signature is computed until storage finishes.” The purpose is to let the sender recover its own action before other nodes can use that signature as consensus evidence.

A Synchronization Request Was Not Enough by Name Alone​

In the September 14 local verification, internal write operations in an actual file-backed H2 store were deliberately delayed. The checks examined when the synchronization call returned and what happened if the calling thread received an interrupt. In that environment, they reproduced paths where the synchronization SQL returning was not enough to establish that pending storage work had finished.

The safety record's synchronization boundary was therefore extended beyond a single SQL call. It keeps the same database connection while waiting for the required storage queues to drain and file synchronization to complete. This implementation is specific to the verified embedded H2 version; the same behavior is not assumed for other versions or storage systems. The RocksDB path's synchronous WAL configuration and failure fence were verified separately.

An interrupt during the wait is not turned into success, either. The caller giving up on waiting does not mean a storage operation that has already started has been canceled. The current H2 path does not return the connection to the pool before confirming actual completion. If an interrupt was observed while waiting, the interrupt status is restored after completion, and the operation is reported as a failure and fenced rather than as a success. This is not a contract that permits forcibly canceling storage once enough time has passed.

Observability becomes important here. If the consensus operation waiting for storage and the status query share the same lock, the request intended to explain the wait may become stuck as well. The verification checked that progress queries still responded while synchronization was held back, and that consensus messages were not broadcast during that interval. A responsive status query helps explain the wait; it does not authorize skipping storage.

An Exception Does Not Mean the Record Is Gone​

The hardest case is when neither success nor failure is certain. If synchronization throws an exception after commit, the record may already be in the database. The caller received a failure, but that cannot be reinterpreted as “nothing was written.” Conversely, reading a row back does not establish that every required synchronization step has finished.

Suppose A reaches this state while writing its record for X. Retrying with Y, or creating a new consensus engine object and carrying on, is not the safe response. The action must not be broadcast, and the safety store whose completion can no longer be trusted must be blocked from reuse.

nigo-protocol applies that block to subsequent reads and writes through the same safety store. Attaching a newly created engine does not bypass the uncertainty in that store. This block is called a fence. It is not a retry marker that clears itself after a short wait. It prevents further consensus actions from being built on a storage result that cannot be trusted.

The scope is this safety store. The fence does not detect every failure across the whole database, nor does it automatically repair a failed storage device. It is not treated as a condition that can be cleared by deleting the WAL or unconditionally restarting the process. After a restart, preserved safety records still need to be recovered and checked so that new actions do not conflict with previous ones.

What the Restart Checks Established​

The September 14 verification examined both the boundary that waits for storage completion and the behavior after failures. It checked that no external broadcast occurred while synchronization was held back, and that, after release, each action was broadcast only after its commit and synchronization completed. Exceptions before and after commit, rollback failures, and synchronization failures were injected to check that reuse of the same safety store was also rejected.

There were also checks that actually terminated a process. The first JVM wrote safety records for a vote and a consensus lock, received completion acknowledgments, and then halted without running normal shutdown hooks. A second JVM opened the same file and recovered the bytes to be signed, the signature, the round, and the consensus evidence already obtained. The checks also confirmed that votes conflicting with the recovered records were rejected and that the state machine could resume.

This answers the question in the opening example directly. Under the process termination conditions tested, a new process could read back actions whose safety writes had been acknowledged. A separate regression using four actual validator nodes and the same build also confirmed that two new transactions finalized after an idle period. That check exercised the normal consensus path after adding the safety boundary.

Process termination, however, is different from cutting power to the computer. Even when the JVM is abruptly halted, the operating system and its caches remain alive. These results are therefore not proof that data will always survive a power failure. Whether a device actually honors synchronization requests, and how the system behaves across separate hosts or under sustained load, require separate verification.

The implementation and verification evidence for this article comes from the local acceptance work at the time, using macOS arm64, Java 21.0.7, H2 2.4.240, and related components. No new failure experiments were performed for this article, and the results do not certify durability across an entire production environment.

Remembering the Promises Sent Outside​

A successful return from the safety record's save function must promise more than that a call has ended. It must include completion of the record's own commit and storage synchronization. If completion cannot be established, further actions must be stoppable. That is what closes the gap between a vote sent outside and the sender's memory after a restart.

This boundary can make waits longer. We therefore need both a way to observe those waits and a rule against proceeding when the storage outcome is uncertain. Showing a delay and deciding whether it is safe to remove that delay are different responsibilities.

Once a restarted node can remember its own actions, the next question is how it catches up with work other nodes have already finalized. Its timer may have advanced to the next round when finality evidence from the previous round arrives late. One of Four Nodes Stalled: Consensus After a Timeout examines how a node can accept valid finality while preserving the rules for creating new signatures.