Repository navigation
FullNode API based on determining chain state #6298
Description
Activity
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 newblock_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:
- eth_getBalance
- eth_getCode
- eth_getStorageAt
- eth_call
-
How many blocks' state are stored in-memory? 18 or 19?
-
@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?
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.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.2. Besides, do you have a plan to support the JSON-RPC which also supports the block parameter? such as:
Of course, JSON-RPC is part of the standard FullNode APIs, where interfaces involving stateful queries are also supported in this issue.
3. How many blocks' state are stored in-memory? 18 or 19?
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.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.@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.@yanghang8612 I agree block number is more user friendly.
@yanghang8612 I agree block number is more user friendly.
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.Nodes must maintain recent block states in memory for a sliding window (e.g., 512 or 1024 blocks)
@yanghang8612 How do you maintain recent blocks' state in memory?
- changed the title
[-]Deterministic state query based on recent block height for TRON APIs[/-][+]FullNode API based on determining chain state[/+]on May 6, 2025 Nodes must maintain recent block states in memory for a sliding window (e.g., 512 or 1024 blocks)
@yanghang8612 How do you maintain recent blocks' state in memory?
Also interested in how to implement it
@waynercheung @King31T There is no definitive way to do this, so feel free to discuss.
18 remaining items
I looked into the current session/snapshot flow. The main challenge for this proposal is establishing a consistent read view for a specific block.
-
Identifying the committed block state
The current in-memory head can include pending transactions or intermediate execution results. Reaching the latest committed block state may require stepping back 0, 1, or 2 snapshot layers, depending on whether the node is idle, accumulating pending transactions, or executing a transaction during pending processing or block production.
This means the target snapshot cannot be located using the block-height difference alone. Temporary session layers and the block commit boundary must also be accounted for.
-
Keeping the entire request on the same state
Once the target block is selected, every read within the request must use that block’s state, even if new transactions or blocks are processed concurrently.
For
triggerconstantcontract, this also includes the block header and execution context: account/storage reads, dynamic properties,block.number, andblock.timestampmust all correspond to the selected block. -
Defining state availability
The supported recent-block window needs an explicit retention policy, including what happens when a state expires while a request is still using it.
Reorganization behavior also needs to be defined. Since the same height can refer to different blocks after a reorg, the selected read view should be associated with a block ID, even when the caller specifies a block number.
A useful first milestone would be reliable committed-head queries. Establishing that read view and its lifetime would provide the foundation for extending queries to other recent blocks.
-
@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.
@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?
-
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.
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.
Loadingflowchart 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"]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.
Loadingflowchart 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 .-> FI 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:
- Simpler queries: requests obtain an existing view without traversing snapshot chains, calculating offsets, or coordinating snapshot capture across stores.
- Clearer consistency: block metadata and all store snapshot references are published together, and each request retains one view.
- Less dependence on storage details: HEAD selection no longer relies on BlockStore’s write marker or the number of temporary session layers.
- Easier lifecycle validation: the rules are explicit—block commit advances HEAD, block pop rolls it back, and temporary execution leaves it unchanged.
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.
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:
- Publish the view atomically only after block commit; restore the corresponding view after block pop and startup recovery.
- Each request captures one view, and all state, VM, and range reads use that same view. Pending or temporary sessions must not mutate the published view.
- Return the resolved block height and block ID, so clients can detect mixed-block results across separate requests.
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.@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?hold references to the existing session layers, with reads walking each store's layer chain down to the root
@alan-eth this way,just like the existing pendingView(the Head)
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Progress
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:
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:
Feature Specification
Affected APIs
This feature applies to all read-only state access methods in FullNode, including but not limited to:
getaccountgetaccountresourcegetdelegatedresourcev2getcontracttriggerconstantcontractAny API that reflects blockchain state is a candidate for deterministic extension.
New Parameters
Each of these APIs will support the following optional parameters:
block_numberintdeterministicbooleantrue, query is resolved strictly against state atblock_number.Behavior
When
deterministicis set totrueandblock_numberis provided:block.timestampandblock.numberreflect the queried block.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_numberis:BLOCK_STATE_UNAVAILABLEBLOCK_NOT_FOUNDdeterministicis false or omitted: fallback to current behaviorNode Requirements
Compatibility
block_numberordeterministicwill see no change in behaviorFuture Considerations
Request for Comments
We welcome feedback from: