Repository navigation
FullNode API based on determining chain state #6298
Description
Background
In the TRON FullNode API suite, a variety of endpoints are provided for reading on-chain state. These include methods such as getaccount, getdelegatedresourcev2, getcontract, triggerconstantcontract, and many others. These interfaces are extensively used by wallets, DApps, indexers, explorers, and backend services to query the current blockchain state.
At present, most read operations are performed against the node's latest in-memory state, which can vary depending on block propagation, mempool contents, and internal node synchronization. While this is sufficient for real-time interaction, it can introduce subtle inconsistencies or non-determinism when:
- State changes between consecutive reads
- State is queried during reorg or finalization windows
- Downstream systems need to reproduce state as it was at a particular block height
To support more robust and reproducible behavior, this proposal introduces deterministic state query capability that allows callers to specify a recent block height to be used as the state root for read operations.
Motivation
Adding deterministic context to state queries can improve:
- Snapshot consistency for voting, airdrops, and retroactive logic
- Test suite repeatability and CI validation for smart contract developers
- UI rendering stability across polling intervals in frontend applications
- Historical verification tools and audit pipelines
- Off-chain computation engines relying on exact state simulation
Feature Specification
Affected APIs
This feature applies to all read-only state access methods in FullNode, including but not limited to:
getaccountgetaccountresourcegetdelegatedresourcev2getcontracttriggerconstantcontract- ...
Any API that reflects blockchain state is a candidate for deterministic extension.
New Parameters
Each of these APIs will support the following optional parameters:
| Parameter | Type | Description |
|---|---|---|
block_number |
int |
Target block height to query against. Must be within the node's recent memory window. |
deterministic |
boolean |
If true, query is resolved strictly against state at block_number. |
Behavior
When deterministic is set to true and block_number is provided:
- The node resolves state using only the data from the specified in-memory block height.
- Dynamic values like
block.timestampandblock.numberreflect the queried block. - No pending transactions or newer block state is included.
- If the requested block has already been finalized and removed from memory, an error such as
BLOCK_STATE_UNAVAILABLEwill be returned.
Example Payloads
getaccount
{
"address": "T...",
"block_number": 71234567,
"deterministic": true
}triggerconstantcontract
{
"owner_address": "T...",
"contract_address": "T...",
"function_selector": "balanceOf(address)",
"parameter": "00000000000000000000000041xxxxxxxx...",
"visible": true,
"block_number": 71234567,
"deterministic": true
}Response Extensions (Optional)
To clarify the execution context, responses MAY include:
{
"execution_mode": "deterministic",
"executed_at_block": 71234567,
"result": { ... }
}Error Handling
If the block specified by block_number is:
- Too old and no longer in memory: return
BLOCK_STATE_UNAVAILABLE - Not yet received by the node: return
BLOCK_NOT_FOUND - Valid but
deterministicis false or omitted: fallback to current behavior
Node Requirements
- Nodes must maintain recent block states in memory for a sliding window (e.g., 512 or 1024 blocks)
- No changes to consensus logic or state mutation logic are needed
- Query cost and latency should remain similar to current behavior for recent blocks
Compatibility
- Fully backward-compatible (new parameters are optional)
- Applies only to a recent window of block state
- Clients not using
block_numberordeterministicwill see no change in behavior
Future Considerations
- Support for finalized blocks or archive-state access beyond in-memory window (e.g., through full archive mode)
- Support for deterministic filters and paginated state views (e.g., account list snapshots)
- Integration with light clients or trustless proof systems
Request for Comments
We welcome feedback from:
- Smart contract developers: Would this capability improve testing or off-chain simulation?
- Wallet and frontend developers: Does this help in offering consistent UI snapshots?
- Node operators: Are there challenges in managing a sliding window of in-memory block state?
- Analytics and indexer providers: Would deterministic reads simplify your pipelines?
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Progress
Activity
waynercheung commentedon Apr 25, 2025
Hi @yanghang8612 , it's a good idea and it is very useful for developers who want to fetch data from a specific block state.
I have questions about the API parameters:
For
block_number, it is only useful whendeterministicis set totrue, right? These two parameters are new, so what about just adding oneblock_numberparameter?If users use the new
block_numberparameter, they are most likely attempting to retrieve data from a specific block state, and thedeterministicparameter may be redundant.Besides, do you have a plan to support the JSON-RPC which also supports the block parameter? such as:
How many blocks' state are stored in-memory? 18 or 19?
SeriousCoding789 commentedon Apr 25, 2025
@yanghang8612 Very good suggestions. Since current this proposal only apply for non-solidated blocks, as non-solidated blocks do have the risk of being forked, so here the deterministic = true will it misleading the users?
SeriousCoding789 commentedon Apr 25, 2025
I would suggest we pass
block_hashonly as the reference withoutdeterministic. As long as there is ablock_hashwe only refer certain data from this block. Even the block later removed by fork, it is still valid as block hash will be changed, but block number is the same.yanghang8612 commentedon Apr 25, 2025
The purpose of adding the additional
deterministicfield is to make the interface design clearer and easier to understand, and to improve compatibility and maintainability with existing interfaces.However, according to the suggestions of @waynercheung and @Sunny6889, it is not necessary to add this field in the definition of the interface, and their main consideration is that this field is redundant on one hand, and misleading to the user in the chain reorganization scenario on the other hand.
It is likely that for most developers, a
block_numberparameter is sufficient to clarify the calling intent of the interface without the need for additional design parameters.yanghang8612 commentedon Apr 25, 2025
@waynercheung
Of course, JSON-RPC is part of the standard FullNode APIs, where interfaces involving stateful queries are also supported in this issue.
In the java-tron design, the number of snapshots in memory is related to the
maxFlushCountfield of the database and the number of unsolidified blocks. Whereas the number of unsolidified blocks is a constant value i.e. 18 (2/3 of the number of SRs) in the mainnet.maxFlushCountparameter tends to be1in FullNode where the synchronization is complete, if we configure this value then the node will have the ability to customize the number of snapshots maintained in memory. This will be very helpful in implementing deterministic state queries in this issue.yanghang8612 commentedon Apr 25, 2025
@Sunny6889 You're right, but why not provide it both ways, then the parameter will become
blockNrOrHash.After all,
numberwill be easy to read and understand, but at the loss of a certain degree of certainty;hashhas absolute certainty, but doesn't conform to the user's reading and understanding habits.Chain reorganization is a small probability event, and the certainty provided by
numberis sufficient. If in some extremely strict certainty requirements of the scene, you can usehash.SeriousCoding789 commentedon Apr 25, 2025
@yanghang8612 I agree block number is more user friendly.
waynercheung commentedon Apr 25, 2025
Yes,
block numberis more friendly for users.And for JSON-RPC, the block parameter can be either block_number, block hash and TAG(
latest,earliest,finalized), so I think we may need to also consider block_hash as a parameter. If block number has been supported, block hash will be more easier.waynercheung commentedon Apr 28, 2025
@yanghang8612 How do you maintain recent blocks' state in memory?
[-]Deterministic state query based on recent block height for TRON APIs[/-][+]FullNode API based on determining chain state[/+]King31T commentedon May 19, 2025
Also interested in how to implement it
yanghang8612 commentedon May 26, 2025
@waynercheung @King31T There is no definitive way to do this, so feel free to discuss.
20 remaining items
yanghang8612 commentedon Sep 11, 2026
@halibobo1205 This aligns with the current direction. Reliable committed-head queries should be the first milestone, so that state selection, request-level consistency, and snapshot lifetime can be established before support is extended to other recent blocks.
The selected view should also remain associated with a specific block ID so that reorganization behavior is handled consistently. Broader retention and recent-block query behavior can be evaluated after the committed-head path has been validated.
halibobo1205 commentedon Sep 11, 2026
@yanghang8612
Thanks for clarifying the direction. I would like to discuss two points about the API semantics for the first milestone:
The consistency guarantees of HEAD
HEAD represents the latest fully committed block on the serving node’s current chain. During block propagation or a reorganization, different nodes may have different heads, including different block IDs at the same height.
Is the intended guarantee that all reads within a single request use the same committed block state? HEAD alone does not guarantee identical results across nodes or block finality. Will responses include the resolved block number and block ID so callers can identify the state used?
Demand for PENDING queries and default behavior
Are there practical use cases that require querying pending state? Do existing clients depend on query results that include the effects of pending transactions?
Will both HEAD and PENDING queries be supported? When no query mode is specified, will the API preserve the current pending-inclusive behavior or default to HEAD?
If the default changes, existing clients may receive different balance or resource results even without changing their requests. How would this align with the proposal’s backward-compatibility goal, and would any versioning or migration arrangements be needed?
Redchar1992 commentedon Sep 16, 2026
From a frontend integration perspective, I would separate two requirements:
Transaction progress: an IDE needs to distinguish broadcast, block inclusion, and solidification. Reading a receipt for an included but not yet solidified transaction does not require exposing state changes from pending transactions. An “unconfirmed” receipt lookup should therefore not, by itself, be counted as evidence of demand for PENDING-state queries.
Consistent application views: a lending UI may combine account, resource, and contract reads across several requests. Request-level committed-HEAD consistency is useful, but it does not make those separate requests an atomic snapshot. Returning the resolved block number and block ID in a backward-compatible form would let clients detect mixed-block results rather than present them as one snapshot.
For the first milestone, I would favor opt-in committed-HEAD reads while preserving existing behavior for requests without an explicit selector. Block-ID-pinned reads across multiple requests can follow later; they need not block the initial milestone.
A useful integration test would advance the chain between related requests and verify that each response identifies its actual committed view, without implying cross-request consistency.
halibobo1205 commentedon Sep 17, 2026
One option is to identify the HEAD snapshot when a query starts, using the presence of a block record in BlockStore.
Pending transaction execution and block-production pre-execution do not write block records. During block application, the block record is written after
processBlockcompletes. We can therefore walk backward from BlockStore’s current snapshot and locate the first layer whose own delta contains a block record.Since each database has its own snapshot chain, we then capture the corresponding session layer from every store. Locating the layer and capturing these references must happen within the same synchronization boundary, so session advancement or rollback cannot invalidate the layer correspondence.
The request then uses those captured references throughout execution.
flowchart TD A["HEAD query arrives"] --> B subgraph CAPTURE["Locate and capture within one synchronization boundary"] B["Get current BlockStore snapshot"] --> C{"Block record in this layer's own delta?"} C -- No --> D["Move to previous snapshot"] D --> C C -- Yes --> E["Capture corresponding snapshots<br/>from all stores and block metadata"] end E --> F["Execute query using the captured view"] F --> G["Return result and clear query context"]halibobo1205 commentedon Sep 17, 2026
Another option is to maintain an explicit HEAD query view as part of the write lifecycle.
The existing
headremains the working pointer, representing PENDING state and any temporary execution changes. A separateHeadViewcontains the corresponding snapshot reference for each store, together with the block ID, height, and timestamp.After a block session successfully completes
commit, the write side constructs and atomically publishes a newHeadView. After a committed block is removed throughpop, it publishes the view corresponding to the restored state. Temporary session creation, pending transaction merges, and temporary session rollback do not update the published HEAD.The view is assembled within the existing serialized write flow. Each query reads the published reference once and uses that view throughout the request, including the VM’s block context. Internal writes and session management continue to use the working pointer.
flowchart TD subgraph WRITE["Write side"] A["Block session commit completes"] --> C B["Committed block pop completes"] --> C C["Collect corresponding store snapshots<br/>and block metadata"] --> D["Build HeadView<br/>and atomically publish HEAD"] end subgraph READ["Query side"] E["HEAD query arrives"] --> F["Read published HeadView once"] F --> G["Use this view for the entire request"] G --> H["Return result and clear query context"] end D -. Published view .-> Fhalibobo1205 commentedon Sep 17, 2026
I currently lean toward maintaining HEAD on the write side. The write path already knows when a block has been committed or rolled back, making these natural points to publish the corresponding query view.
The main benefits are:
The implementation needs to cover commit, pop, and initialization after startup recovery. Both approaches also require all relevant reads, including range queries, to respect the selected view. The second approach simplifies how that view is selected and made available to requests.
Redchar1992 commentedon Sep 22, 2026
The write-side
HeadViewlooks like the cleaner first milestone. It keeps snapshot selection in the serialized write lifecycle instead of making every query traverse storage details.To make the contract testable, I would make these invariants explicit:
This should be documented as committed-HEAD consistency, not cross-request atomicity or arbitrary historical
block_numbersupport. The latter can build on the same snapshot abstraction in a later milestone. Requests without an explicit selector should retain their existing behavior.alan-eth commentedon Sep 24, 2026
@halibobo1205 The write-side
HeadViewmakes view selection really clean. A quick question on the query side: does the view hold references to the existing session layers, with reads walking each store's layer chain down to the root, or is it a merged copy built at publish time?halibobo1205 commentedon Sep 24, 2026
@alan-eth this way,just like the existing pendingView(the Head)
halibobo1205 commentedon Oct 13, 2026
Quick update: after reviewing the [v4.8.3 release plan](#6957), we've decided not to include this feature in v4.8.3 and to revisit it for a later release.
This feature involves substantial changes to the FullNode APIs and the underlying state-access layer. We want to take more time to align on the query semantics—including committed-HEAD behavior, request-level consistency, snapshot retention, and reorg/finalization handling—and validate the implementation before committing it to a release.
The issue will remain open, and we'll continue following up on the design and implementation for a future version. We welcome further discussion and feedback. @yanghang8612 @alan-eth cc @SeriousCoding789 @Murphytron