Skip to content

[Feature] Record internal transactions for value transfers to precompiled contracts #42

Description

@bladehan1

Summary

When a smart contract successfully calls a precompiled contract with a positive TRX or TRC10
value, generate a corresponding internal transaction for the balance transfer. This allows
ProgramResult.internalTransactions, the TransactionInfo API, and the Transaction Event Plugin to
fully reflect value transfers that occur during execution.

This feature only supplements locally derived node data. It does not change balance transfers,
contract state, transaction execution results, Energy consumption, or block consensus.

Problem

Motivation

Normal contract calls record an internal transaction when a value transfer occurs. Calls to
precompiled contracts can perform equivalent TRX or TRC10 balance transfers, but currently do not
produce a corresponding record.

Providing consistent internal transaction data for both execution paths allows TransactionInfo
and Event Plugin consumers to observe the complete execution process. It also enables indexing,
auditing, and accounting systems to process contract-to-contract transfers uniformly.

Current State

When a smart contract successfully calls a precompiled contract with a positive TRX or TRC10
value, java-tron completes and commits the balance transfer but does not add the corresponding
entry to ProgramResult.internalTransactions.

This behavior is part of TVM execution and applies to all networks. It can also be observed in a
private test network or unit test. To verify it through the TransactionInfo API,
vm.saveInternalTx = true must be enabled. Alternatively, inspect
ProgramResult.internalTransactions directly or enable transaction event output that includes
the internal transaction list.

Limitations or Risks

The current behavior has the following limitations:

  • The TransactionInfo API cannot expose these transfers even though they occurred during
    execution.
  • Transaction Event subscribers receive an incomplete internal transaction list.
  • Accounting and indexing systems cannot observe precompiled-contract transfers using the same
    data-processing path used for normal contract calls.
  • Actual balance changes are not fully represented by the derived internal transaction list.

Proposed Solution

Proposed Design

After a successful CALL or CALLTOKEN to a precompiled contract with a positive value, add one
successful internal transaction with the following properties:

  • note is set to call.
  • The caller is the address of the calling contract.
  • The transfer-to address is the precompiled contract address.
  • A TRX transfer records its amount in callValue; a TRC10 transfer records its token ID and amount
    in tokenInfo.
  • The record uses the next internal transaction nonce within the current external transaction.
  • rejected is set to false.

A zero-value call or a failed precompiled-contract execution does not generate a successful value
transfer record.

If the precompiled-contract call succeeds and generates an internal transaction, but its enclosing
call frame or the entire external transaction later fails because of an exception or REVERT, the
record must behave like every other internal transaction in the same failed execution path. The
record remains in the list, the corresponding balance change is rolled back with the state, and
the existing failure-propagation mechanism sets rejected to true. The record must neither be
removed nor remain marked as successful.

In Program.callToPrecompiledAddress, after the precompiled contract returns successfully and
before committing the child repository:

  1. Check whether endowment > 0.
  2. Increment the internal transaction nonce scoped to the current external transaction.
  3. For a TRC10 transfer, construct tokenInfo with the token ID and transferred amount.
  4. Add an internal transaction using the caller address, precompiled contract address, input data,
    call note, and current nonce.
  5. Commit the repository as before.

When execution fails, neither the child repository nor the transfer is committed, so no successful
internal transaction should be added.

If the precompiled-contract call succeeds but a later enclosing execution fails, the existing
ProgramResult.rejectInternalTransactions() behavior should apply without special handling for
this record.

Key Changes

  • Module: TVM / Program.callToPrecompiledAddress
  • API: No new or modified API; the existing TransactionInfo API returns the new record when
    vm.saveInternalTx is enabled
  • Event: Transaction Event Plugin output includes the new record
  • Configuration: No new configuration; continue using vm.saveInternalTx
  • Protocol: No TRON protocol change, block-version gate, or proposal parameter
  • Database: No new database or field

An internal transaction nonce is scoped to one external transaction. Each root VM execution starts
at nonce 0; nested calls inherit the current nonce and return their final nonce to the parent.

The nonce is not persisted as an independent database field. When vm.saveInternalTx is enabled,
the hash derived from the nonce is stored with TransactionInfo.internal_transactions. For newly
processed blocks, TransactionInfo records are grouped by block in TransactionRetStore. The query
path reads that store first and then falls back to the legacy txid-keyed
TransactionHistoryStore. There is no separate internal transaction database.

Impact

  • Consensus and state: No impact; transaction execution results, account state, and block
    consensus remain unchanged
  • Security: No new permissions or fund-transfer logic
  • Performance: Each successful non-zero-value precompiled call constructs one additional
    derived record; such calls are expected to be uncommon
  • Developer experience: Value transfers to normal and precompiled contracts can be observed
    consistently
  • Indexing: Indexers receive more complete transfer data from TransactionInfo or the
    Transaction Event Plugin

java-tron does not provide a dedicated endpoint for looking up a record by its internal transaction
hash. However, gettransactioninfobyid includes InternalTransaction.hash in the returned
internal_transactions, and the Transaction Event Plugin also exposes this field. The hash is
therefore observable and may be used by external indexers.

Add unit tests covering:

  • A successful TRX transfer and its internal transaction fields.
  • A successful TRC10 transfer and its tokenInfo fields.
  • A zero-value call producing no transfer record.
  • A failed precompiled-contract execution producing no successful transfer record.
  • A successful precompiled-contract call followed by failure of its enclosing call frame or
    external transaction, verifying that the balance transfer is rolled back and the generated
    record remains present with rejected set to true, consistently with other internal
    transactions in the same failed execution path.
  • An explicit comparison of the old and new behavior, showing that an otherwise identical later
    internal transaction uses nonce 1 under the old behavior and nonce 2 under the new behavior, and
    therefore has a different hash.

Compatibility

  • Breaking Change: No
  • Default Behavior Change: Yes. After upgrading, a successful TRX or TRC10 value transfer to a
    precompiled contract produces an additional internal transaction
  • Migration Required: No

This feature takes effect when the node software is upgraded. It does not introduce a block-version
gate or proposal parameter.

For the same external transaction, upgraded and non-upgraded nodes may return different internal
transaction lists. The new record consumes a transaction-scoped nonce, so hashes of later internal
transactions in the same external transaction may also differ. Systems that use internal
transaction hashes for cross-node reconciliation should account for node software versions or
avoid mixing upgraded and non-upgraded nodes as a single data source.

The release notes should disclose:

After upgrading, a successful TRX or TRC10 value transfer to a precompiled contract produces an
additional internal transaction. For the same external transaction, upgraded and non-upgraded
nodes may return different internal transaction lists. Because the new record consumes a
transaction-scoped nonce, hashes of later internal transactions in that external transaction may
also differ. This change does not affect transaction execution results, account state, or block
consensus.

References (Optional)

The official TRONSCAN API documentation provides Transaction Detail and Internal Transaction List
endpoints, with the latter returning internal_hash. If upgraded and non-upgraded java-tron nodes
are mixed as ingestion sources, the same external transaction may have:

  • A different number of internal transaction records.
  • One additional precompile value-transfer record on an upgraded node.
  • A different internal_hash for an otherwise identical later internal transaction because its
    transaction-scoped nonce is shifted by one.

TRONSCAN likely serves indexed or transferred data rather than querying a FullNode for every page
view. However, its internal ingestion architecture, node failover strategy, deduplication keys, and
historical backfill policy are not defined in the java-tron repository or public API documentation
and require confirmation from the TRONSCAN team. If ingestion for the same height range can switch
between nodes running different code versions, the ingestion pipeline should pin a java-tron
version or deduplicate and reconcile records by external transaction and execution order. It should
not assume that internal_hash remains stable across node versions during the rollout.

Related issues: None identified.

Additional Notes

  • Do you have ideas regarding implementation? Yes
  • Are you willing to implement this feature? Yes

The following activation approaches were considered:

  1. Activate on node upgrade. This has the lowest long-term network-wide maintenance cost. For a
    small number of affected transactions, upgraded and non-upgraded nodes may temporarily return
    different internal transaction lists and hashes during the rollout.
  2. Use a block-version gate. This can coordinate activation among upgraded nodes, but cannot
    eliminate differences when an older node continues following the chain and derives
    TransactionInfo using the old behavior.
  3. Use a proposal parameter. This provides the strongest upgrade boundary because old code
    cannot validate the new proposal parameter, but introduces a one-time governance parameter and
    permanent maintenance surface for a small non-consensus data-consistency benefit.

Considering the scope of impact and maintenance cost, activation on node upgrade is recommended.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions