Skip to content

Commit f926a72

Browse files
committed
prune correctly stem
Signed-off-by: Karim Taam <karim.t2am@gmail.com>
1 parent dd07450 commit f926a72

5 files changed

Lines changed: 223 additions & 8 deletions

File tree

src/main/java/org/hyperledger/besu/ethereum/stateless/bintrie/SimpleBinTrie.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.hyperledger.besu.ethereum.stateless.bintrie.node.LeafNode;
2121
import org.hyperledger.besu.ethereum.stateless.bintrie.node.Node;
2222
import org.hyperledger.besu.ethereum.stateless.bintrie.node.NullNode;
23+
import org.hyperledger.besu.ethereum.stateless.bintrie.pruning.StemPrunableNodeRegistry;
2324
import org.hyperledger.besu.ethereum.stateless.bintrie.visitor.CommitVisitor;
2425
import org.hyperledger.besu.ethereum.stateless.bintrie.visitor.FlattenVisitor;
2526
import org.hyperledger.besu.ethereum.stateless.bintrie.visitor.GetVisitor;
@@ -30,6 +31,7 @@
3031

3132
import java.util.Optional;
3233

34+
import org.apache.tuweni.bytes.Bytes;
3335
import org.apache.tuweni.bytes.Bytes32;
3436

3537
/**
@@ -39,11 +41,13 @@
3941
* @param <V> The type of values in the Bin Trie.
4042
*/
4143
public class SimpleBinTrie<K extends BitSequence<K>, V> implements BinTrie<K, V> {
44+
45+
private final StemPrunableNodeRegistry<K> stemPrunableNodeRegistry;
4246
protected Node<K, V> root;
4347

4448
/** Creates a new Bin Trie with a null node as the root. */
4549
public SimpleBinTrie() {
46-
this.root = NullNode.node();
50+
this(NullNode.node());
4751
}
4852

4953
/**
@@ -52,7 +56,7 @@ public SimpleBinTrie() {
5256
* @param root The root node of the Bin Trie.
5357
*/
5458
public SimpleBinTrie(final Optional<Node<K, V>> root) {
55-
this.root = root.orElse(NullNode.node());
59+
this(root.orElse(NullNode.node()));
5660
}
5761

5862
/**
@@ -62,6 +66,7 @@ public SimpleBinTrie(final Optional<Node<K, V>> root) {
6266
*/
6367
public SimpleBinTrie(final Node<K, V> root) {
6468
this.root = root;
69+
this.stemPrunableNodeRegistry = new StemPrunableNodeRegistry<>();
6570
}
6671

6772
/**
@@ -99,7 +104,7 @@ public Optional<V> get(final K key) {
99104
public Optional<V> put(final K key, final V value) {
100105
checkNotNull(key);
101106
checkNotNull(value);
102-
final PutVisitor<K, V> kvPutVisitor = new PutVisitor<>(key, value);
107+
final PutVisitor<K, V> kvPutVisitor = new PutVisitor<>(key, value, stemPrunableNodeRegistry);
103108
this.root = root.accept(kvPutVisitor);
104109
return kvPutVisitor.getOldValue();
105110
}
@@ -118,7 +123,7 @@ public void remove(final K key) {
118123
/** Restructure tree to get minimal representation. */
119124
@Override
120125
public void flatten() {
121-
this.root = root.accept(new FlattenVisitor<K, V>());
126+
this.root = root.accept(new FlattenVisitor<K, V>(stemPrunableNodeRegistry));
122127
}
123128

124129
/**
@@ -128,7 +133,7 @@ public void flatten() {
128133
*/
129134
@Override
130135
public Bytes32 getRootHash() {
131-
root = root.accept(new FlattenVisitor<K, V>());
136+
root = root.accept(new FlattenVisitor<K, V>(stemPrunableNodeRegistry));
132137
root = root.accept(new HashVisitor<K, V>());
133138
assert root.commitment.isPresent() : "HashVisitor should produce a rootHash";
134139
return root.commitment.get();
@@ -152,6 +157,14 @@ public String toString() {
152157
@Override
153158
public void commit(final NodeUpdater nodeUpdater) {
154159
getRootHash();
160+
stemPrunableNodeRegistry
161+
.getPrunableStems()
162+
.forEach(
163+
stem -> {
164+
nodeUpdater.store(Bytes.wrap(stem.encode()), null, null);
165+
});
166+
167+
stemPrunableNodeRegistry.clear();
155168
root = root.accept(new CommitVisitor<K, V>(nodeUpdater));
156169
}
157170

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Hyperledger Besu Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*
15+
*/
16+
package org.hyperledger.besu.ethereum.stateless.bintrie.pruning;
17+
18+
import org.hyperledger.besu.ethereum.stateless.bintrie.BitSequence;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
/**
24+
* Registry responsible for tracking {@code StemNode} stems that are eligible for pruning.
25+
*
26+
* <p>When a {@code StemNode} becomes obsolete (e.g., replaced by a {@code NullNode}), its stem can
27+
* be marked as prunable and later removed during a cleanup pass. This class centralizes that
28+
* tracking mechanism.
29+
*/
30+
public class StemPrunableNodeRegistry<K extends BitSequence<K>> {
31+
32+
private final List<K> prunableStems = new ArrayList<>();
33+
34+
/** Creates a new registry for tracking prunable stem nodes. */
35+
public StemPrunableNodeRegistry() {}
36+
37+
/**
38+
* Marks a stem as prunable, indicating that the corresponding {@code StemNode} can be safely
39+
* removed if no longer needed.
40+
*
41+
* @param key the stem key to mark as prunable
42+
*/
43+
public void markPrunableStem(final K key) {
44+
prunableStems.add(key);
45+
}
46+
47+
/**
48+
* Removes a stem from the prunable set, usually because it has been revived or re-added to the
49+
* trie.
50+
*
51+
* @param key the stem key to remove from pruning consideration
52+
*/
53+
public void removePrunableStem(final K key) {
54+
prunableStems.remove(key);
55+
}
56+
57+
/**
58+
* Returns the list of all stems currently marked as prunable.
59+
*
60+
* @return list of stems eligible for pruning
61+
*/
62+
public List<K> getPrunableStems() {
63+
return prunableStems;
64+
}
65+
66+
/** Clears all tracked prunable stems. Typically used after a batch prune operation. */
67+
public void clear() {
68+
prunableStems.clear();
69+
}
70+
}

src/main/java/org/hyperledger/besu/ethereum/stateless/bintrie/visitor/FlattenVisitor.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.hyperledger.besu.ethereum.stateless.bintrie.node.Node;
2121
import org.hyperledger.besu.ethereum.stateless.bintrie.node.NullNode;
2222
import org.hyperledger.besu.ethereum.stateless.bintrie.node.StemNode;
23+
import org.hyperledger.besu.ethereum.stateless.bintrie.pruning.StemPrunableNodeRegistry;
2324

2425
/**
2526
* Class representing a visitor for flattening a node in a Trie tree.
@@ -32,6 +33,13 @@
3233
* @param <V> The type of node values.
3334
*/
3435
public class FlattenVisitor<K extends BitSequence<K>, V> implements NodeVisitor<K, V> {
36+
37+
private final StemPrunableNodeRegistry<K> stemPrunableNodeRegistry;
38+
39+
public FlattenVisitor(final StemPrunableNodeRegistry<K> stemPrunableNodeRegistry) {
40+
this.stemPrunableNodeRegistry = stemPrunableNodeRegistry;
41+
}
42+
3543
@Override
3644
public Node<K, V> visit(InternalNode<K, V> internalNode) {
3745

@@ -71,6 +79,7 @@ public Node<K, V> visit(StemNode<K, V> stemNode) {
7179
return stemNode;
7280
}
7381
if (stemNode.allLeavesAreNull()) {
82+
stemPrunableNodeRegistry.markPrunableStem(stemNode.stem);
7483
return NullNode.node();
7584
}
7685
return stemNode;

src/main/java/org/hyperledger/besu/ethereum/stateless/bintrie/visitor/PutVisitor.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.hyperledger.besu.ethereum.stateless.bintrie.node.NullNode;
2424
import org.hyperledger.besu.ethereum.stateless.bintrie.node.StemNode;
2525
import org.hyperledger.besu.ethereum.stateless.bintrie.node.ValueNode;
26+
import org.hyperledger.besu.ethereum.stateless.bintrie.pruning.StemPrunableNodeRegistry;
2627

2728
import java.util.Optional;
2829

@@ -35,6 +36,9 @@
3536
* @param <V> The type of values to insert or update.
3637
*/
3738
public class PutVisitor<K extends BitSequence<K>, V> implements NodeVisitor<K, V> {
39+
40+
private final StemPrunableNodeRegistry<K> stemPrunableNodeRegistry;
41+
3842
public final K path;
3943
private Optional<V> oldValue;
4044
public final V value;
@@ -44,12 +48,16 @@ public class PutVisitor<K extends BitSequence<K>, V> implements NodeVisitor<K, V
4448
* Constructs a new PutVisitor with the provided value to insert or update.
4549
*
4650
* @param value The value to be inserted or updated in the Verkle Trie.
51+
* @param stemPrunableNodeRegistry Tracker of stem to delete
4752
*/
48-
public PutVisitor(final K path, final V value) {
53+
public PutVisitor(
54+
final K path, final V value, final StemPrunableNodeRegistry<K> stemPrunableNodeRegistry) {
4955
assert path.length() <= Node.KEY_SIZE;
5056
this.path = path;
5157
this.value = value;
5258
this.oldValue = Optional.empty();
59+
this.stemPrunableNodeRegistry = stemPrunableNodeRegistry;
60+
5361
// System.out.println(String.format("PutVisit path=%s, value=%s",
5462
// Bytes.wrap(path.toBytes()),
5563
// value));
@@ -134,8 +142,9 @@ public Node<K, V> visit(final StemNode<K, V> stemNode) {
134142
public Node<K, V> visit(final NullNode<K, V> nullNode) {
135143
// System.out.println(String.format("Put visit Null: depth=%s, loc=%s, stem=%s", depth+1,
136144
// path.slice(0, depth+1).toHexString(), path.slice(0, Node.STEM_SIZE).toHexString()));
137-
return new StemNode<K, V>(Optional.of(path.slice(0, depth + 1)), path.slice(0, Node.STEM_SIZE))
138-
.accept(this);
145+
final K stem = path.slice(0, Node.STEM_SIZE);
146+
stemPrunableNodeRegistry.removePrunableStem(stem);
147+
return new StemNode<K, V>(Optional.of(path.slice(0, depth + 1)), stem).accept(this);
139148
}
140149

141150
/**
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright Hyperledger Besu Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*
15+
*/
16+
package org.hyperledger.besu.ethereum.stateless.bintrie;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import org.hyperledger.besu.ethereum.stateless.bintrie.factory.StoredNodeFactory;
21+
22+
import org.apache.tuweni.bytes.Bytes;
23+
import org.apache.tuweni.bytes.Bytes32;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.DisplayName;
26+
import org.junit.jupiter.api.Test;
27+
28+
@DisplayName("StemPrunableNodeRegistryTest – stem pruning behaviour")
29+
class StemPrunableNodeRegistryTest {
30+
31+
private static final BytesBitSequenceFactory KEY_FACTORY = new BytesBitSequenceFactory();
32+
private static final BytesBitSequence KEY =
33+
KEY_FACTORY.fromHexString(
34+
"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff");
35+
private static final Bytes32 VALUE =
36+
Bytes32.fromHexString("0x1000000000000000000000000000000000000000000000000000000000000000");
37+
private static final Bytes STEM =
38+
Bytes.fromHexString(
39+
"0x00112233445566778899aabbccddeeff00112233445566778899aabbccddee00"); // first 31 bytes
40+
41+
private org.hyperledger.besu.ethereum.stateless.bintrie.NodeUpdaterMock nodeUpdater;
42+
private StoredBinTrie<BytesBitSequence, Bytes32> trie;
43+
44+
@BeforeEach
45+
void setUp() {
46+
nodeUpdater = new NodeUpdaterMock();
47+
NodeLoaderMock nodeLoader = new NodeLoaderMock(nodeUpdater.storage);
48+
StoredNodeFactory<BytesBitSequence, Bytes32> nodeFactory =
49+
new StoredNodeFactory<>(nodeLoader, KEY_FACTORY, x -> (Bytes32) x);
50+
trie = new StoredBinTrie<>(nodeFactory);
51+
}
52+
53+
@Test
54+
@DisplayName("prunes stem after key is removed and committed")
55+
void shouldPruneStemAfterRemoveAndCommit() {
56+
// put + commit
57+
trie.put(KEY, VALUE);
58+
trie.commit(nodeUpdater);
59+
assertThat(stemExists()).isTrue();
60+
assertThat(trie.getRootHash()).isNotEqualTo(Bytes32.ZERO);
61+
62+
// remove + second commit
63+
trie.remove(KEY);
64+
trie.commit(nodeUpdater);
65+
66+
assertThat(stemExists()).isFalse();
67+
assertThat(trie.getRootHash()).isEqualTo(Bytes32.ZERO);
68+
}
69+
70+
@Test
71+
@DisplayName("keeps stem when key is removed then re-added before commit")
72+
void shouldKeepStemWhenKeyRemovedAndReaddedBeforeSameCommit() {
73+
// put + commit
74+
trie.put(KEY, VALUE);
75+
trie.commit(nodeUpdater);
76+
assertThat(stemExists()).isTrue();
77+
78+
// remove + put (same transaction) + commit
79+
trie.remove(KEY);
80+
trie.put(KEY, VALUE);
81+
trie.commit(nodeUpdater);
82+
83+
assertThat(stemExists()).isTrue();
84+
assertThat(trie.getRootHash()).isNotEqualTo(Bytes32.ZERO);
85+
}
86+
87+
@Test
88+
@DisplayName("keeps stem when key is re-added in a later transaction")
89+
void shouldKeepStemWhenKeyReaddedAfterSeparateCommit() {
90+
// put + commit
91+
trie.put(KEY, VALUE);
92+
trie.commit(nodeUpdater);
93+
assertThat(stemExists()).isTrue();
94+
95+
// remove + commit ➜ stem prunable
96+
trie.remove(KEY);
97+
trie.commit(nodeUpdater);
98+
assertThat(stemExists()).isFalse();
99+
100+
// put again + commit ➜ stem revived
101+
trie.put(KEY, VALUE);
102+
trie.commit(nodeUpdater);
103+
104+
assertThat(stemExists()).isTrue();
105+
assertThat(trie.getRootHash()).isNotEqualTo(Bytes32.ZERO);
106+
}
107+
108+
/**
109+
* @return {@code true} if the current in-memory DB still contains the stem.
110+
*/
111+
private boolean stemExists() {
112+
return nodeUpdater.storage.keySet().stream().anyMatch(k -> k.equals(STEM));
113+
}
114+
}

0 commit comments

Comments
 (0)