-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Summary
TrieImpl.insert() violates the Merkle Trie invariant: the same dataset should always produce the same root hash, regardless of insertion order. When a duplicate key-value pair is inserted (same key, same value), the node is unconditionally marked as dirty, which invalidates the internal hash cache and causes the final root hash to become insertion-order-dependent.
Root Cause
In TrieImpl.java, the kvNodeSetValueOrNode() method (L873-878) unconditionally sets dirty = true without checking whether the new value equals the existing value:
public Node kvNodeSetValueOrNode(Object valueOrNode) {
parse();
assert getType() != NodeType.BranchNode;
children[1] = valueOrNode; // overwrites even if same value
dirty = true; // always marks dirty
return this;
}This is triggered from insert() (L188-189) when commonPrefix.equals(k):
} else if (commonPrefix.equals(k)) {
return n.kvNodeSetValueOrNode(nodeOrValue); // no same-value check
}Reproduction
The existing TrieTest.testOrder() (currently @Ignore) demonstrates the issue:
- Insert keys 1-99, then re-insert key 10 with the same value
- Shuffle the insertion order and rebuild
- Root hashes differ ~3% of the time depending on shuffle order
Impact
Current: None. The allowAccountStateRoot chain parameter (proposal 25) has not been enabled on mainnet. AccountStateCallBack checks allowAccountStateRoot() before using TrieImpl, so this code path is never executed.
Future: Consensus-level risk. If proposal 25 is approved, AccountStateCallBack will use TrieImpl to compute per-block account state root hashes stored in block headers. Different nodes could compute different root hashes for the same block if duplicate trie.put(address, state) calls occur, leading to BadBlockException and potential chain forks.
Suggested Fix
Add a same-value check in kvNodeSetValueOrNode() to short-circuit when the value hasn't changed:
public Node kvNodeSetValueOrNode(Object valueOrNode) {
parse();
assert getType() != NodeType.BranchNode;
if (valueOrNode instanceof byte[] && children[1] instanceof byte[]
&& Arrays.equals((byte[]) valueOrNode, (byte[]) children[1])) {
return this; // no change, skip dirty marking
}
children[1] = valueOrNode;
dirty = true;
return this;
}Then remove the @Ignore on TrieTest.testOrder() to verify the fix.
Related
- PR test(framework): add dual DB engine coverage with flaky test fixes #6586 —
@Ignoreannotation and root cause analysis inTrieTest.java AccountStateCallBack.java— production consumer ofTrieImpl