Skip to content

Commit d16f5bd

Browse files
committed
ci: equivalence — normalize compaction state before diff (closes #168 root cause)
Phase-2 investigation of the account-asset/contract divergence (run 26677752647: 6/8 stores byte-identical incl. account 3.6M + storage-row 17M; account-asset Go 27,917 vs Java 27,965; contract Go 560,890 vs Java 561,120). Investigated on a real Nile snapshot (EC2 10.255.10.72) with a goleveldb SST dumper decoding internal-key sequence numbers + types. ROOT CAUSE (not a mutation bug): the account-asset and contract stores carry DELETE tombstones + multi-version keys from normal java-tron operation. account-asset: 27,850 distinct keys, 51 with a DELETE tombstone as their NEWEST version, 330 multi-version entries -> 27,799 live. goleveldb's DB.Iterator returns EXACTLY 27,799 (= 27,850 - 51), proving goleveldb correctly omits tombstones + resolves multi-version to newest-seq. account (3.6M) and storage-row (17M) have no such cruft and matched byte-for-byte. Both tools start from the identical fixture. Go's Apply opens stores via goleveldb (compaction-on-open) and drops more of the already-deleted / obsolete entries; java DbFork (leveldbjni) leaves the store less compacted and physically retains them. The "java-only" keys are DELETED keys Go correctly drops and java retains -- a PHYSICAL compaction difference of logically-identical state (both boot java-tron to the same chain state; deleted keys stay deleted). goleveldb does the correct, safe-direction compaction. FIX: before the byte diff, force a full goleveldb CompactRange of every dbfork store on BOTH scratch dirs, converging differing physical compaction states to the canonical live, newest-seq, tombstone-free form. Verified on-box: goleveldb CompactRange of an account-asset store with 51 tombstones converges it to exactly the 27,799-key live set. Real mutation differences survive compaction (it changes physical layout, not logical content), so genuine divergences are still caught. Full analysis + numbers in task #168.
1 parent dc3593f commit d16f5bd

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

internal/dbfork/equivalence_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"strings"
1313
"testing"
1414

15+
"github.com/syndtr/goleveldb/leveldb"
16+
"github.com/syndtr/goleveldb/leveldb/util"
1517
"google.golang.org/protobuf/encoding/prototext"
1618
"google.golang.org/protobuf/proto"
1719

@@ -171,6 +173,25 @@ func TestEquivalence_GoVsJava(t *testing.T) {
171173
}
172174
t.Logf("equivalence: java DbFork output:\n%s", javaOut)
173175

176+
// Normalize physical compaction state before the byte diff. The two
177+
// tools produce the same LOGICAL state but different PHYSICAL layouts
178+
// of pre-existing data: Go's Apply opens stores via goleveldb
179+
// (compaction-on-open, which drops already-deleted DELETE tombstones
180+
// and resolves multi-version keys to the newest sequence number)
181+
// while java DbFork (leveldbjni) leaves the store less-compacted and
182+
// physically retains deleted/obsolete entries. Without normalizing,
183+
// the diff fails on those tombstoned/overwritten PRE-EXISTING keys —
184+
// not on any mutation. Forcing a full goleveldb CompactRange on every
185+
// store on BOTH sides converges them to the canonical live newest-seq
186+
// / tombstone-free form, so the diff reflects only logical (mutation)
187+
// differences. Verified on a real Nile snapshot (#168): goleveldb
188+
// CompactRange of an account-asset store with 51 tombstones converges
189+
// it to exactly the 27,799-key live set. Real mutation differences
190+
// survive compaction (it changes physical layout, not logical
191+
// content), so genuine divergences are still caught.
192+
normalizeViaCompaction(t, scratchGo)
193+
normalizeViaCompaction(t, scratchJava)
194+
174195
// Diff each of the 8 dbfork stores. Order is deterministic
175196
// (stores.AllStores) so a failing run always reports the same
176197
// store first, making CI bisection easier.
@@ -181,6 +202,31 @@ func TestEquivalence_GoVsJava(t *testing.T) {
181202
}
182203
}
183204

205+
// normalizeViaCompaction forces a full goleveldb CompactRange of every
206+
// dbfork store under root/database, converging differing physical
207+
// compaction states of the same logical data to the canonical live,
208+
// newest-sequence, tombstone-free form. See the call site + #168.
209+
func normalizeViaCompaction(t *testing.T, root string) {
210+
t.Helper()
211+
for _, store := range stores.AllStores {
212+
dir := filepath.Join(root, "database", store)
213+
if _, err := os.Stat(dir); err != nil {
214+
continue // store legitimately absent (lite snapshot pruning)
215+
}
216+
ldb, err := leveldb.OpenFile(dir, nil)
217+
if err != nil {
218+
t.Fatalf("equivalence: normalize-open %s in %s: %v", store, root, err)
219+
}
220+
if err := ldb.CompactRange(util.Range{}); err != nil {
221+
ldb.Close()
222+
t.Fatalf("equivalence: normalize-compact %s in %s: %v", store, root, err)
223+
}
224+
if err := ldb.Close(); err != nil {
225+
t.Fatalf("equivalence: normalize-close %s in %s: %v", store, root, err)
226+
}
227+
}
228+
}
229+
184230
// javaHeapFlag returns the JVM `-Xmx<size>` flag for the equivalence
185231
// invocation. Defaults to `-Xmx4g` which is enough for Nile lite
186232
// snapshots; CI on smaller runners can override via DBFORK_JAVA_HEAP

0 commit comments

Comments
 (0)