@@ -50,6 +50,7 @@ import (
5050 "github.com/erigontech/erigon/db/kv/mdbx"
5151 "github.com/erigontech/erigon/db/kv/order"
5252 "github.com/erigontech/erigon/db/kv/stream"
53+ "github.com/erigontech/erigon/db/recsplit"
5354 "github.com/erigontech/erigon/db/seg"
5455 "github.com/erigontech/erigon/db/state/changeset"
5556 "github.com/erigontech/erigon/db/state/statecfg"
@@ -3103,3 +3104,119 @@ func TestDomain_IntegrateDirtyFilesNilGuard(t *testing.T) {
31033104 require .NotNil (t , foundAfter , "dirty file for step 0 must still exist after nil StaticFiles" )
31043105 require .NotNil (t , foundAfter .decompressor , "dirty file decompressor must not be overwritten by nil StaticFiles" )
31053106}
3107+
3108+ // filledDomainWithHashMapAccessor creates a domain configured to use AccessorHashMap
3109+ // (like CommitmentDomain) for testing buildHashMapAccessor code paths.
3110+ func filledDomainWithHashMapAccessor (t * testing.T , logger log.Logger ) (kv.RwDB , * Domain , uint64 ) {
3111+ t .Helper ()
3112+ dirs := datadir2 .New (t .TempDir ())
3113+
3114+ // Start with AccountsDomain config but switch to HashMap accessor
3115+ cfg := statecfg .Schema .AccountsDomain
3116+ cfg .Accessors = statecfg .AccessorHashMap // Use HashMap instead of BTree
3117+
3118+ // Set version to V1_0_standart to enable HashMap accessor building
3119+ cfg .FileVersion = statecfg.DomainVersionTypes {
3120+ DataKV : version .V1_0_standart ,
3121+ AccessorBT : version .V1_0_standart ,
3122+ AccessorKVEI : version .V1_0_standart ,
3123+ AccessorKVI : version .V1_0_standart ,
3124+ }
3125+ cfg .Hist .IiCfg .FileVersion = statecfg.IIVersionTypes {
3126+ DataEF : version .V1_0_standart ,
3127+ AccessorEFI : version .V1_0_standart ,
3128+ }
3129+
3130+ db := mdbx .New (dbcfg .ChainDB , logger ).InMem (t , dirs .Chaindata ).MustOpen ()
3131+ t .Cleanup (db .Close )
3132+ salt := uint32 (1 )
3133+
3134+ d , err := NewDomain (cfg , 16 , config3 .DefaultStepsInFrozenFile , dirs , logger )
3135+ require .NoError (t , err )
3136+ d .salt .Store (& salt )
3137+ d .DisableFsync ()
3138+ t .Cleanup (d .Close )
3139+
3140+ txs := fillDomain (t , d , db , logger )
3141+ return db , d , txs
3142+ }
3143+
3144+ // collateAndMergeWithCollisionRetry is like collateAndMerge but forces a
3145+ // recsplit collision retry on every buildHashMapAccessor call during merge.
3146+ func collateAndMergeWithCollisionRetry (t * testing.T , tx kv.RwTx , d * Domain , txs uint64 ) {
3147+ t .Helper ()
3148+ logEvery := time .NewTicker (30 * time .Second )
3149+ defer logEvery .Stop ()
3150+ ctx := context .Background ()
3151+
3152+ // Collate without collision forcing first
3153+ for step := kv .Step (0 ); step < kv .Step (txs / d .stepSize )- 1 ; step ++ {
3154+ require .NoError (t , d .collateBuildIntegrate (ctx , step , tx , background .NewProgressSet ()))
3155+ }
3156+
3157+ // Now set up collision forcing for merge
3158+ d ._testBuildAccessorHook = func (rs * recsplit.RecSplit ) {
3159+ rs .ForceCollisionOnce ()
3160+ }
3161+
3162+ domainRoTx := d .BeginFilesRo ()
3163+ defer domainRoTx .Close ()
3164+
3165+ // Merge with collision retry
3166+ r := domainRoTx .findMergeRange (d .dirtyFilesEndTxNumMinimax (), d .dirtyFilesEndTxNumMinimax ())
3167+ if r .values .needMerge {
3168+ valuesOuts , indexOuts , historyOuts := domainRoTx .staticFilesInRange (r )
3169+ valuesIn , indexIn , historyIn , err := domainRoTx .mergeFiles (ctx , valuesOuts , indexOuts , historyOuts , r , nil , background .NewProgressSet ())
3170+ require .NoError (t , err )
3171+ d .integrateMergedDirtyFiles (valuesIn , indexIn , historyIn )
3172+ d .reCalcVisibleFiles (d .dirtyFilesEndTxNumMinimax ())
3173+ }
3174+ }
3175+
3176+ // TestDomain_KeyPosResetOnCollisionRetry verifies that keyPos and valPos
3177+ // used in buildHashMapAccessor are reset when the build retries due to a
3178+ // recsplit collision. Without the reset, the .kvi index would contain
3179+ // incorrect offsets on the retry pass.
3180+ func TestDomain_KeyPosResetOnCollisionRetry (t * testing.T ) {
3181+ if testing .Short () {
3182+ t .Skip ()
3183+ }
3184+
3185+ t .Parallel ()
3186+
3187+ logger := log .New ()
3188+ db , d , txs := filledDomainWithHashMapAccessor (t , logger )
3189+
3190+ ctx := context .Background ()
3191+ tx , err := db .BeginRw (ctx )
3192+ require .NoError (t , err )
3193+ defer tx .Rollback ()
3194+
3195+ // Collate and merge with forced collision retry
3196+ collateAndMergeWithCollisionRetry (t , tx , d , txs )
3197+ require .NoError (t , tx .Commit ())
3198+
3199+ // Verify lookups still work correctly after collision retry
3200+ roTx , err := db .BeginRo (ctx )
3201+ require .NoError (t , err )
3202+ defer roTx .Rollback ()
3203+
3204+ domainRoTx := d .BeginFilesRo ()
3205+ defer domainRoTx .Close ()
3206+
3207+ // Check that we can look up keys correctly
3208+ // If keyPos wasn't reset, the index would have wrong offsets
3209+ for keyNum := uint64 (1 ); keyNum <= uint64 (31 ); keyNum ++ {
3210+ var k [8 ]byte
3211+ binary .BigEndian .PutUint64 (k [:], keyNum )
3212+
3213+ val , _ , found , err := domainRoTx .GetLatest (k [:], roTx )
3214+ require .NoError (t , err , "key %x" , k )
3215+ require .True (t , found , "key %x should be found" , k )
3216+
3217+ // Expected value is txs/keyNum
3218+ var expected [8 ]byte
3219+ binary .BigEndian .PutUint64 (expected [:], txs / keyNum )
3220+ require .Equal (t , expected [:], val , "key %x value mismatch" , k )
3221+ }
3222+ }
0 commit comments