Skip to content

FullNode API based on determining chain state #6298

Description

@yanghang8612

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:

  • getaccount
  • getaccountresource
  • getdelegatedresourcev2
  • getcontract
  • triggerconstantcontract
  • ...

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.timestamp and block.number reflect 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_UNAVAILABLE will 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 deterministic is 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_number or deterministic will 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?

Activity

  1. waynercheung commented on Apr 25, 2025

    @waynercheung
    Collaborator

    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:

    1. For block_number , it is only useful when deterministic is set to true, right? These two parameters are new, so what about just adding one block_number parameter?
      If users use the new block_number parameter, they are most likely attempting to retrieve data from a specific block state, and the deterministic parameter may be redundant.

    2. 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
    3. How many blocks' state are stored in-memory? 18 or 19?

  2. SeriousCoding789 commented on Apr 25, 2025

    @SeriousCoding789
    Contributor

    @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?

  3. SeriousCoding789 commented on Apr 25, 2025

    @SeriousCoding789
    Contributor

    I would suggest we pass block_hash only as the reference without deterministic. As long as there is a block_hash we 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.

  4. yanghang8612 commented on Apr 25, 2025

    @yanghang8612
    CollaboratorAuthor

    The purpose of adding the additional deterministic field 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_number parameter is sufficient to clarify the calling intent of the interface without the need for additional design parameters.

  5. yanghang8612 commented on Apr 25, 2025

    @yanghang8612
    CollaboratorAuthor

    @waynercheung

    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 maxFlushCount field 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. maxFlushCount parameter tends to be 1 in 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.

  6. yanghang8612 commented on Apr 25, 2025

    @yanghang8612
    CollaboratorAuthor

    I would suggest we pass block_hash only as the reference without deterministic. As long as there is a block_hash we 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, number will be easy to read and understand, but at the loss of a certain degree of certainty; hash has 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 number is sufficient. If in some extremely strict certainty requirements of the scene, you can use hash.

  7. SeriousCoding789 commented on Apr 25, 2025

    @SeriousCoding789
    Contributor

    @yanghang8612 I agree block number is more user friendly.

  8. waynercheung commented on Apr 25, 2025

    @waynercheung
    Collaborator

    @yanghang8612 I agree block number is more user friendly.

    Yes, block number is 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.

  9. waynercheung commented on Apr 28, 2025

    @waynercheung
    Collaborator

    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?

  10. 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
  11. King31T commented on May 19, 2025

    @King31T

    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

  12. yanghang8612 commented on May 26, 2025

    @yanghang8612
    CollaboratorAuthor

    @waynercheung @King31T There is no definitive way to do this, so feel free to discuss.

  13. 18 remaining items

  14. halibobo1205 commented on Sep 10, 2026

    @halibobo1205
    Collaborator

    I looked into the current session/snapshot flow. The main challenge for this proposal is establishing a consistent read view for a specific block.

    1. 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.

    2. 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, and block.timestamp must all correspond to the selected block.

    3. 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.

  15. yanghang8612 commented on Sep 11, 2026

    @yanghang8612
    CollaboratorAuthor

    @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.

  16. halibobo1205 commented on Sep 11, 2026

    @halibobo1205
    Collaborator

    @yanghang8612
    Thanks for clarifying the direction. I would like to discuss two points about the API semantics for the first milestone:

    1. 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?

    2. 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?

  17. Redchar1992 commented on Sep 16, 2026

    @Redchar1992

    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.

  18. halibobo1205 commented on Sep 17, 2026

    @halibobo1205
    Collaborator

    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 processBlock completes. 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"]
    
    Loading
  19. halibobo1205 commented on Sep 17, 2026

    @halibobo1205
    Collaborator

    Another option is to maintain an explicit HEAD query view as part of the write lifecycle.

    The existing head remains the working pointer, representing PENDING state and any temporary execution changes. A separate HeadView contains 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 new HeadView. After a committed block is removed through pop, 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 .-> F
    
    Loading
  20. halibobo1205 commented on Sep 17, 2026

    @halibobo1205
    Collaborator

    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:

    • 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.

  21. moved this from Backlog to In Progress in java-tronon Sep 22, 2026
  22. Redchar1992 commented on Sep 22, 2026

    @Redchar1992

    The write-side HeadView looks 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_number support. The latter can build on the same snapshot abstraction in a later milestone. Requests without an explicit selector should retain their existing behavior.

  23. removed this from the GreatVoyage-v4.8.3 milestone on Sep 24, 2026
  24. alan-eth commented on Sep 24, 2026

    @alan-eth

    @halibobo1205 The write-side HeadView makes 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?

  25. halibobo1205 commented on Sep 24, 2026

    @halibobo1205
    Collaborator

    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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

  • Status
    In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions