Absent Then, Unavailable Now: Two Different Answers About Historical State
You ask a ledger, “Did this asset exist then?” The answer comes back: the record cannot be found. Does that mean it really did not exist at that point? Or does the node answering the question lack the data from that time?
The distinction is easy to overlook when checking a current balance. But it matters when establishing past ownership or auditing a transaction's outcome. If we interpret unavailable data as evidence of absence, the facts of the ledger depend on the storage circumstances of whichever node we ask.
Preserving the same ledger under parallel execution examined why different completion orders must still produce the same ledger result. This article asks what happens afterward. Once time has passed, can we still establish what the ledger contained then?
A's current balance of 80 is not enough
Suppose A holds 100 units of an asset and sends 20 to B. Nigo can represent an asset's owner and quantity in a record called a StateCell. In this example, the transfer consumes an existing record and creates new ones.
A block is a unit that contains the results of transactions in order. Its height is its position in the block sequence.
At the end of block 10, a record called u0 held A's 100 units.
Assume nothing happens to this asset at heights 11 through 19, and the transfer takes place in block 20.
The transaction consumes u0, creates u1 holding A's 80 units, and creates u2 holding B's 20 units.
The names u0, u1, and u2 simply distinguish the records in this example.
Assume there are no fees, no additional issuance or burning, and no other records of this asset belonging to A or B.
At the end of block 20, u1 and u2 remain, totaling 100. The transaction did not overwrite u0's quantity with 80. It removed u0 from the currently usable state. Its past existence must be established separately.
Reading only the current state reveals A's 80 and B's 20.
But answering “Did A hold 100 at the end of block 10?” requires data from that point.
The absence of u0 now does not tell us that it was also absent ten blocks ago.
First, narrow the question to one record: “Did u0 exist at the end of block 10, and did it contain A's 100 units?”
Knowing the assumptions of this example lets us relate that record to A's balance.
On a real ledger, establishing that one record exists is separate from finding every record belonging to A and summing them without omissions.
Fix the reference state before asking about the past
“Height 10” alone is not a sufficient basis for verification. We must also fix which block we mean and which state remained after it was processed.
Nigo's historical state queries specify the block height, block hash, and state root together. Hashing turns content into a summary of a fixed length, which can be used to check for changes in that content. The block hash identifies the block. The state root summarizes the ledger state at that point by combining hashes. It provides a reference against which to check a retrieved record.
From here on, “the state at height 10” means a request with all three values fixed. If the node has captured the history at that height, it checks that the requested block hash and state root match its stored references. A mismatch causes it to reject the query. If it has no captured history at that height, it must first report that the data has not been captured. Turning a request with a mismatched reference into an absence result would make a question about the wrong state look as if it had been handled correctly. Conversely, if several blocks leave the state unchanged and therefore share a root, that alone does not make their points in the block sequence identical.
The unit of time also matters. What is stored and checked here is the state after a finalized block has finished.
This does not mean its end state contains every intermediate record that transactions within the block created and then consumed.
Whether u0 briefly existed after a particular transaction in block 10 is a different question from whether it remained at the end of that block.
Change history, past values, and proofs serve different purposes
The transfer's transaction record lets us follow the connection from consuming u0 to creating u1 and u2.
Lineage, discussed in StateCell ownership and execution boundaries,
describes these relationships: which transactions created and changed a state.
These connections help answer “How did we get here?” But finding a transaction's inputs and outputs does not automatically prove that a record was active at an arbitrary past point. That requires checking it against the state at the end of the relevant block.
Historical queries need data with several distinct roles.
- The original Cell data from that time tells us what was actually recorded, including the owner and quantity.
- The state root and verification path from that time provide the basis for checking that the data belongs to the requested state.
- The range of history captured by the node tells us whether it has the material needed to answer for that point.
A state root is not a compressed archive that can be unpacked into the original data. Keeping only the summary does not let us recover past owners and quantities. Conversely, even when the original data survives, we still need a basis for establishing that it was included in the state at the point we are asking about.
Nodes using Nigo's persistent storage keep historical Cell data and records of their captured coverage.
Consuming u0 from the current state does not also remove this stored historical data.
When applying a new block, the node updates the current state and that block's historical data in the same storage operation.
This prevents it from updating only the current state while incorrectly marking the history for that point as captured.
Not every node has received all history from the beginning, however. A node can join using a snapshot, a collection of state
at a particular height, and start from there. A node that starts from the state at height 20 may hold the current u1 and u2.
It cannot therefore claim to have received u0's original data and verification material from the earlier height 10.
Separate “it was absent” from “I cannot provide it”
Consider querying u0 again. Even for the same record, the meaning of the answer depends on the requested point and the node's
storage circumstances. These are illustrative cases, rather than results from actual API calls.
| Request and node circumstances | Meaning of the answer |
|---|---|
| u0 at height 10. The node has captured the required data and permits the query | u0 existed then and contained A's 100 units |
| u0 at height 20. The node has captured the required data and permits the query | u0 was absent from the active state then |
| u0 at height 10. The node started from a snapshot at height 20 | The node cannot answer because it has not captured data from that point |
| u0 at height 10. The node captured the history, but its current retention policy does not permit the query | This node does not currently provide data for that point |
The first two answers describe the ledger's state. The other two describe whether the responding node can provide the data. Nodes processing the same current ledger can answer different ranges of historical questions because they started at different points or use different retention policies.
Nigo distinguishes these outcomes as FOUND, ABSENT, NOT_CAPTURED, and NOT_RETAINED, respectively.
The meaning of ABSENT matters more than the name. It does not simply mean that a database search returned nothing.
It means the absence of that Cell ID has been verified against the requested state root.
A proof that u0 is absent at height 20 does not prove that it never existed, or that the record of the transaction consuming it was also deleted.
Likewise, NOT_RETAINED should not be read as “already deleted from disk.”
The data may still physically exist while the current retention policy disallows queries against that state.
Whether data may be queried and whether its physical deletion is complete are separate decisions.
Some situations must not be placed in any of these categories. Suppose history at a point has been captured and is currently retained,
but required verification material or original Cell data is corrupt or missing. That must be treated as a query or verification failure.
Reporting ABSENT because damaged data could not be found would disguise a storage problem as a fact about the ledger.
The node must not silently substitute a current value or a transaction record from another point, either.
How to check the server's answer yourself
Suppose a server with the necessary data answers, “At height 10, u0 contained A's 100 units.”
Being able to read the answer is different from being able to verify it.
Nigo's single Cell proofs use a path through a Merkle tree, which combines hashes to summarize state. Each record's identifier determines its position. Hashes of the record contents are combined from the bottom up to form the state root. Instead of receiving the entire ledger, a verifier receives the record of interest and the hashes from neighboring branches needed to calculate the root from that position.
An answer stating that u0 exists at height 10 can be checked in this order.
- Check that the response refers to the requested block, state root, and
u0. - Decode the received Cell data, check its ID and contents, and hash its value using the specified format.
- Starting at the position determined by
u0's ID, combine the hashes in the verification path. - Compare the calculated root with the state root you intended to check.
Establishing that a record belongs to a state in this way is a membership proof. In this example, the original data also binds the claim that A owns the record and its quantity is 100. Changing only the quantity while presenting the same path changes the hash, so that path no longer produces the reference root.
For u0 at height 20, the check instead establishes that the position for that ID is empty, following the path up to the root.
This is a non-membership proof.
It supplies verifiable evidence that the record is not included in the specified state, beyond a server's claim that it could not find it.
Nigo supplies proofs for verified existence and absence. It does not present an answer about uncaptured data as a proof of absence.
This verification does not require reconnecting to the server's database. However, the reference state root must be independently trustworthy. Someone could invent a ledger and provide a matching root and proof; the calculations could still agree. Establishing that the block and root represent the finalized result of the ledger you intend to check is a separate responsibility of block and consensus verification. The Cell proof itself does not guarantee the block's finality.
Nigo provides single Cell proofs that deliver stored original data together with a verification path. The unit discussed here is one particular Cell in one particular state. It must not be generalized into a proof of A's total assets, or a proof that A held the same quantity throughout a period.
Verifying a proof you received and obtaining it again
Suppose you receive the original data and proof for u0 at height 10 and save them with the reference root.
Even if the node later stops allowing queries at height 10, the calculation that checks the saved material against the same root does not change.
To verify it again, you need the saved proof and its reference. You do not need a promise that the node still serves the same data.
The situation is different if another user first requests the same material a few days later. Someone must keep storing and serving the original Cell data and the material needed to construct the proof. Receiving one response does not automatically preserve that state forever. Being able to verify a past fact is separate from being able to keep obtaining the data when it is needed.
The data also needs protection while a proof is being prepared. If cleanup deletes a required tree node during proof generation, the reader cannot obtain the complete original data and verification path. Nigo coordinates queries and cleanup so that required material cannot be deleted while an allowed query checks the retention criteria, reads the data, and verifies it. Read protection ends once the data has been obtained and verified; the response is then constructed from that material. This protection does not promise that the same state will remain available for future queries.
From here, the proof algorithm alone cannot answer the questions. Storage and operational policies must determine how far back the node will serve data, which states need special retention, and what it will report when data cannot be provided.
Returning to the question of ownership over time
The review of a time-weighted ownership contract explored recording not just current balances, but who held assets and for how long. It also raised a question: could the ledger provide a shared history capability instead of each asset repeatedly building its own?
Historical Cell queries and proofs provide one foundation for answering that question.
In our example, checking u0 at height 10 and u1 and u2 at height 20 lets us describe the records at two points.
Those two sets of proofs alone do not establish that nothing changed between them.
The absence of changes at heights 11 through 19 was an assumption supplied by this example.
Calculating an actual holding period requires more: the intervening changes, a record range with no omissions, and rules for interpreting the start and end of the period. Quantities split across multiple Cells also need to be summed. A single Cell proof does not perform that calculation. It does let us recheck which state contained a particular historical value used in it.
Answering the opening question required us to separate the meanings of “there is no record.” The record might have been absent from the ledger then, the node might never have received the data from that point, or it might not provide that data now. Alongside keeping historical values, a ledger must make clear what evidence supports its answers and how far its ability to answer extends.
That leaves the cost of storage. How can we distinguish and delete material that is no longer needed while protecting what is required for the proofs we must serve? If old states share some of their verification material, can we delete all related data merely because one state is no longer retained? This leads from historical state queries to the design of garbage collection (GC): reclaiming stored material that is no longer needed.