Skip to content

fix(trie): make TrieImpl.insert() idempotent for duplicate key-value#6696

Open
halibobo1205 wants to merge 3 commits intotronprotocol:developfrom
halibobo1205:fix/trie-insert-order-bug
Open

fix(trie): make TrieImpl.insert() idempotent for duplicate key-value#6696
halibobo1205 wants to merge 3 commits intotronprotocol:developfrom
halibobo1205:fix/trie-insert-order-bug

Conversation

@halibobo1205
Copy link
Copy Markdown
Collaborator

@halibobo1205 halibobo1205 commented Apr 20, 2026

Summary

Fix two bugs in TrieImpl.insert() that together break Merkle Trie idempotency when the same key is put more than once (regardless of whether the value changes):

  1. Correctness (root-hash corruption). Reorder the condition checks in insert() so thatcommonPrefix.equals(k) is evaluated before commonPrefix.isEmpty(). The two conditions overlap only when both k and currentNodeKey are empty, which the recursion reaches exclusively on a duplicate put of an already-inserted key on a fully-split KV leaf. In that state, the old order incorrectly triggered the "no common prefix" branch and replaced KVNode("", v_old) with BranchNode{terminal: v_new}, corrupting the trie structure and, in turn, the root hash. This happens whether or not the new value equals the old one.

  2. Optimization (avoid unnecessary recomputation). Short-circuit kvNodeSetValueOrNode() when the new byte[] value equals the existing one (by reference or byte content) to skip redundant dirty marking and downstream hash recomputation on the KV-leaf path. This does not by itself fix the root-hash corruption; it only reduces cache churn. Branch-terminal writes and Node-valued writes are intentionally not short-circuited (parent nodes still need dirty propagation when a child is mutated in place).

Closes #6608.

When the bug triggers

  • ❌ Every key put at most once per trie → not triggered, no impact.
  • ✅ Same key put ≥ 2 times (same or different value) → triggered,root hash becomes insertion-order-dependent.

The only production path into TrieImpl is AccountStateCallBack. Its trieEntryList is an undeduplicated ArrayList: if multiple transactions touch an account in the same block, `trie.put(addr, …) is called multiple times with the same key, hitting bug 1.

Consensus impact and release scope

AccountStateCallBack is gated behind proposal 25 (ALLOW_ACCOUNT_STATE_ROOT), which has not been activated on mainnet or nile. Therefore, this fix has no impact on the current consensus.

If proposal 25 is ever activated, AccountStateCallBack will compute per-block account-state roots stored in block headers. Without this fix, two honest nodes processing the same block — where any account is modified by more than one transaction — could compute different state roots and fork. This fix must therefore be merged and released before any release that could run a chain on which proposal 25 may activate.

  • Hard requirement: MUST be included in any release shipped before or
    during ALLOW_ACCOUNT_STATE_ROOT activation.

See #6608 for the full reproduction and affected code paths.

…puts

Reorder condition checks in insert() so that commonPrefix.equals(k) is
evaluated before commonPrefix.isEmpty(). When both k and currentNodeKey
are empty (which happens on a duplicate put of a fully-split key), the
old order incorrectly fired the "no common prefix" branch and replaced
KVNode("",v) with BranchNode{terminal:v}, corrupting the root hash.

Also short-circuit kvNodeSetValueOrNode() when the new value equals the
existing one (by reference or by byte content) to avoid unnecessary
dirty marking and downstream hash recomputation.

close tronprotocol#6608
Comment thread framework/src/main/java/org/tron/core/trie/TrieImpl.java
Comment thread framework/src/main/java/org/tron/core/trie/TrieImpl.java
Comment thread framework/src/test/java/org/tron/core/tire/TrieTest.java Outdated
Comment thread framework/src/test/java/org/tron/core/tire/TrieTest.java Outdated
Comment thread framework/src/test/java/org/tron/core/tire/TrieTest.java
@waynercheung
Copy link
Copy Markdown
Collaborator

waynercheung commented Apr 20, 2026

[MUST] The consensus-impact analysis is already documented in #6608, but per the repo’s consensus-change requirements, please mirror a short summary into the PR description itself and explicitly document:

  1. the target release / upgrade scope for this fix;
  2. that this fix must be included in any release intended to run before / when Proposal 25 (ALLOW_ACCOUNT_STATE_ROOT) may activate.

A short summary referencing #6608 is sufficient; no need to restate the full analysis.

Optional cleanup: since this PR already touches TrieTest.java, it would also be nice to remove the two pre-existing System.out.println lines at TrieTest.java:67-68 in this PR if convenient. If you prefer to keep this PR tightly scoped, a small follow-up cleanup PR is also fine.

@halibobo1205
Copy link
Copy Markdown
Collaborator Author

@waynercheung

  1. The PR description is updated.
  2. TrieTest.java:67-68 is cleaned up.

@halibobo1205 halibobo1205 added the topic:DB Database label Apr 21, 2026
@halibobo1205 halibobo1205 added this to the GreatVoyage-v4.8.2 milestone Apr 21, 2026
@lvs0075
Copy link
Copy Markdown
Collaborator

lvs0075 commented Apr 22, 2026

I have a question regarding the description: the current code does not affect the consensus. The modification is merely intended to ensure that the results of the random write are consistent.

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

Labels

topic:DB Database

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: TrieImpl.insert() is not idempotent for duplicate key-value pairs

4 participants