@@ -37,6 +37,7 @@ import (
3737 "github.com/XinFinOrg/XDPoSChain/crypto"
3838 "github.com/XinFinOrg/XDPoSChain/ethdb"
3939 "github.com/XinFinOrg/XDPoSChain/params"
40+ "github.com/XinFinOrg/XDPoSChain/rlp"
4041)
4142
4243var (
@@ -50,17 +51,30 @@ var (
5051 },
5152 BaseFee : big .NewInt (params .InitialBaseFee ),
5253 }
53- signer = types .LatestSigner (gspec .Config )
5454)
5555
5656type testEnv struct {
5757 chain * core.BlockChain
5858 pool * txpool.TxPool
5959 tracker * TxTracker
6060 genDb ethdb.Database
61+ signer types.Signer
6162}
6263
6364func newTestEnv (t * testing.T , n int , gasTip uint64 , journal string ) * testEnv {
65+ return newTestEnvWithConfig (t , n , gasTip , journal , params .TestChainConfig )
66+ }
67+
68+ // newTestEnvWithConfig builds an environment around cfg. It builds its own
69+ // genesis and signer instead of touching the package level ones, which the
70+ // tests in this file share.
71+ func newTestEnvWithConfig (t * testing.T , n int , gasTip uint64 , journal string , cfg * params.ChainConfig ) * testEnv {
72+ gspec := & core.Genesis {
73+ Config : cfg ,
74+ Alloc : types.GenesisAlloc {address : {Balance : new (big.Int ).Set (funds )}},
75+ BaseFee : big .NewInt (params .InitialBaseFee ),
76+ }
77+ signer := types .LatestSigner (cfg )
6478 genDb , blocks , _ := core .GenerateChainWithGenesis (gspec , ethash .NewFaker (), n , func (i int , gen * core.BlockGen ) {
6579 gasPrice := big .NewInt (params .InitialBaseFee )
6680 if baseFee := gen .BaseFee (); baseFee != nil {
@@ -95,7 +109,23 @@ func newTestEnv(t *testing.T, n int, gasTip uint64, journal string) *testEnv {
95109 pool : pool ,
96110 tracker : New (journal , time .Minute , gspec .Config , pool ),
97111 genDb : genDb ,
112+ signer : signer ,
113+ }
114+ }
115+
116+ // nonce returns the next nonce the test account can spend at the current head.
117+ func (env * testEnv ) nonce () uint64 {
118+ head := env .chain .CurrentHeader ()
119+ state , _ := env .chain .StateAt (head .Root )
120+ return state .GetNonce (address )
121+ }
122+
123+ // gasPrice returns a gas price the gas schedule of the current head admits.
124+ func (env * testEnv ) gasPrice () * big.Int {
125+ if baseFee := env .chain .CurrentHeader ().BaseFee ; baseFee != nil {
126+ return new (big.Int ).Set (baseFee )
98127 }
128+ return big .NewInt (params .InitialBaseFee )
99129}
100130
101131func (env * testEnv ) close () {
@@ -116,7 +146,7 @@ func (env *testEnv) makeTxs(n int) []*types.Transaction {
116146
117147 var txs []* types.Transaction
118148 for i := 0 ; i < n ; i ++ {
119- tx , _ := types .SignTx (types .NewTransaction (nonce + uint64 (i ), common.Address {0x00 }, big .NewInt (1000 ), params .TxGas , gasPrice , nil ), signer , key )
149+ tx , _ := types .SignTx (types .NewTransaction (nonce + uint64 (i ), common.Address {0x00 }, big .NewInt (1000 ), params .TxGas , gasPrice , nil ), env . signer , key )
120150 txs = append (txs , tx )
121151 }
122152 return txs
@@ -225,3 +255,265 @@ func TestStartContinuesOnCorruptedJournal(t *testing.T) {
225255 t .Fatal ("Journal writer should be initialized even if journal load fails" )
226256 }
227257}
258+
259+ // replacementPair returns two transactions sharing a nonce: the one tracked
260+ // first, and the one that replaces it.
261+ func replacementPair (env * testEnv ) (replaced , replacement * types.Transaction ) {
262+ nonce := env .nonce ()
263+ mk := func (to common.Address ) * types.Transaction {
264+ tx , _ := types .SignTx (types .NewTransaction (nonce , to , big .NewInt (1000 ), params .TxGas , env .gasPrice (), nil ), env .signer , key )
265+ return tx
266+ }
267+ return mk (common.Address {0x00 }), mk (common.Address {0x01 })
268+ }
269+
270+ // writeReplacementJournal writes a journal holding two transactions that share
271+ // a nonce, the way an older version could have left it behind: a transaction
272+ // together with the one that replaced it, in the order selected by reverse.
273+ func writeReplacementJournal (t * testing.T , reverse bool ) (path string , replaced , replacement * types.Transaction ) {
274+ t .Helper ()
275+
276+ env := newTestEnv (t , 10 , 0 , "" )
277+ defer env .close ()
278+
279+ replaced , replacement = replacementPair (env )
280+ order := []* types.Transaction {replaced , replacement }
281+ if reverse {
282+ order = []* types.Transaction {replacement , replaced }
283+ }
284+ var journal []byte
285+ for _ , tx := range order {
286+ blob , err := rlp .EncodeToBytes (tx )
287+ if err != nil {
288+ t .Fatalf ("Failed to encode transaction: %v" , err )
289+ }
290+ journal = append (journal , blob ... )
291+ }
292+ path = filepath .Join (t .TempDir (), fmt .Sprintf ("%d" , rand .Int63 ()))
293+ if err := os .WriteFile (path , journal , 0o644 ); err != nil {
294+ t .Fatalf ("Failed to write journal: %v" , err )
295+ }
296+ return path , replaced , replacement
297+ }
298+
299+ // TestTrackAllKeepsTransactionHeldByPool pins which transaction wins a nonce
300+ // when the pool already holds one of them: the pool decides, because the order
301+ // transactions reach TrackAll is not the order they were accepted in. Add
302+ // tracks a local transaction only after SubPool.Add has released its lock, so
303+ // a replacement can arrive here before the transaction it replaced.
304+ func TestTrackAllKeepsTransactionHeldByPool (t * testing.T ) {
305+ for _ , tc := range []struct {
306+ name string
307+ reversed bool
308+ }{
309+ {name : "pooled first" },
310+ {name : "replacement first" , reversed : true },
311+ } {
312+ t .Run (tc .name , func (t * testing.T ) {
313+ env := newTestEnv (t , 10 , 0 , "" )
314+ defer env .close ()
315+
316+ pooled , replacement := replacementPair (env )
317+ if err := env .pool .Add ([]* types.Transaction {pooled }, true )[0 ]; err != nil {
318+ t .Fatalf ("failed to add the transaction the pool must hold: %v" , err )
319+ }
320+ pair := []* types.Transaction {pooled , replacement }
321+ if tc .reversed {
322+ pair = []* types.Transaction {replacement , pooled }
323+ }
324+ env .tracker .TrackAll (pair )
325+
326+ env .tracker .mu .Lock ()
327+ defer env .tracker .mu .Unlock ()
328+
329+ if len (env .tracker .all ) != 1 {
330+ t .Fatalf ("tracked set must hold a single transaction, got %d" , len (env .tracker .all ))
331+ }
332+ if _ , ok := env .tracker .all [pooled .Hash ()]; ! ok {
333+ t .Fatalf ("the transaction the pool holds must stay tracked: %v" , pooled .Hash ())
334+ }
335+ if kept := env .tracker .byAddr [address ].Get (pooled .Nonce ()); kept == nil || kept .Hash () != pooled .Hash () {
336+ t .Fatalf ("nonce %d must hold the pooled transaction, got %v" , pooled .Nonce (), kept )
337+ }
338+ })
339+ }
340+ }
341+
342+ // TestConcurrentReplacementKeepsPoolTransaction pins the tracker's view of two
343+ // concurrent submissions for the same nonce: the original is added first and
344+ // reaches the tracker last, the window AddLocal leaves between SubPool.Add
345+ // releasing its lock and Track acquiring this one. The transaction the pool
346+ // holds must win the nonce even though it is tracked last, or recheck would
347+ // keep resubmitting the superseded one and the live one would lose the local
348+ // resubmit protection entirely.
349+ func TestConcurrentReplacementKeepsPoolTransaction (t * testing.T ) {
350+ env := newTestEnv (t , 10 , 0 , "" )
351+ defer env .close ()
352+
353+ nonce := env .nonce ()
354+ price := env .gasPrice ()
355+ // The replacement has to clear the pool's price bump to take the nonce.
356+ bumped := new (big.Int ).Div (
357+ new (big.Int ).Mul (price , big .NewInt (int64 (100 + legacypool .DefaultConfig .PriceBump ))),
358+ big .NewInt (100 ))
359+ bumped .Add (bumped , big .NewInt (1 ))
360+ mk := func (to common.Address , gasPrice * big.Int ) * types.Transaction {
361+ tx , _ := types .SignTx (types .NewTransaction (nonce , to , big .NewInt (1000 ), params .TxGas , gasPrice , nil ), env .signer , key )
362+ return tx
363+ }
364+ var (
365+ original = mk (common.Address {0x00 }, price )
366+ replacement = mk (common.Address {0x01 }, bumped )
367+ added = make (chan struct {})
368+ tracked = make (chan struct {})
369+ finished = make (chan error , 2 )
370+ )
371+ go func () {
372+ err := env .pool .Add ([]* types.Transaction {original }, true )[0 ]
373+ close (added )
374+ <- tracked
375+ env .tracker .Track (original )
376+ finished <- err
377+ }()
378+ go func () {
379+ <- added
380+ err := env .pool .Add ([]* types.Transaction {replacement }, true )[0 ]
381+ env .tracker .Track (replacement )
382+ close (tracked )
383+ finished <- err
384+ }()
385+ for i := 0 ; i < 2 ; i ++ {
386+ if err := <- finished ; err != nil {
387+ t .Fatalf ("failed to submit the transaction: %v" , err )
388+ }
389+ }
390+
391+ env .tracker .mu .Lock ()
392+ defer env .tracker .mu .Unlock ()
393+
394+ if len (env .tracker .all ) != 1 {
395+ t .Fatalf ("tracked set must hold a single transaction, got %d" , len (env .tracker .all ))
396+ }
397+ if _ , ok := env .tracker .all [replacement .Hash ()]; ! ok {
398+ t .Fatalf ("the replacement the pool holds must stay tracked: %v" , replacement .Hash ())
399+ }
400+ }
401+
402+ func TestTrackAllDropsReplacedTransaction (t * testing.T ) {
403+ env := newTestEnv (t , 10 , 0 , "" )
404+ defer env .close ()
405+
406+ replaced , replacement := replacementPair (env )
407+ env .tracker .TrackAll ([]* types.Transaction {replaced , replacement })
408+
409+ env .tracker .mu .Lock ()
410+ defer env .tracker .mu .Unlock ()
411+
412+ if len (env .tracker .all ) != 1 {
413+ t .Fatalf ("replaced transaction must be dropped: tracking %d" , len (env .tracker .all ))
414+ }
415+ if _ , ok := env .tracker .all [replaced .Hash ()]; ok {
416+ t .Fatalf ("replaced transaction still tracked: %v" , replaced .Hash ())
417+ }
418+ kept := env .tracker .byAddr [address ].Get (replacement .Nonce ())
419+ if kept == nil || kept .Hash () != replacement .Hash () {
420+ t .Fatalf ("nonce %d must hold the replacement, got %v" , replacement .Nonce (), kept )
421+ }
422+ }
423+
424+ func TestRecheckDoesNotResubmitReplacedTransaction (t * testing.T ) {
425+ env := newTestEnv (t , 10 , 0 , "" )
426+ defer env .close ()
427+
428+ replaced , replacement := replacementPair (env )
429+ env .tracker .TrackAll ([]* types.Transaction {replaced , replacement })
430+
431+ resubmits := env .tracker .recheck (false )
432+ if len (resubmits ) != 1 || resubmits [0 ].Hash () != replacement .Hash () {
433+ t .Fatalf ("unexpected transactions to resubmit: %v" , resubmits )
434+ }
435+ }
436+
437+ func TestJournalRotationDropsReplacedTransaction (t * testing.T ) {
438+ journalPath := filepath .Join (t .TempDir (), fmt .Sprintf ("%d" , rand .Int63 ()))
439+ env := newTestEnv (t , 10 , 0 , journalPath )
440+ defer env .close ()
441+
442+ if err := env .tracker .Start (); err != nil {
443+ t .Fatalf ("Failed to start tracker: %v" , err )
444+ }
445+ defer env .tracker .Stop ()
446+
447+ replaced , replacement := replacementPair (env )
448+ env .tracker .TrackAll ([]* types.Transaction {replaced , replacement })
449+
450+ // Rotate the journal from the tracked set: the replaced transaction has
451+ // been dropped from it and must not come back.
452+ env .tracker .recheck (true )
453+
454+ reloaded := New (journalPath , time .Minute , params .TestChainConfig , env .pool )
455+ if err := reloaded .journal .load (func (transactions []* types.Transaction ) []error {
456+ reloaded .TrackAll (transactions )
457+ return nil
458+ }); err != nil {
459+ t .Fatalf ("Failed to load journal: %v" , err )
460+ }
461+
462+ reloaded .mu .Lock ()
463+ defer reloaded .mu .Unlock ()
464+
465+ if len (reloaded .all ) != 1 {
466+ t .Fatalf ("rotated journal must hold a single transaction, got %d" , len (reloaded .all ))
467+ }
468+ if _ , ok := reloaded .all [replacement .Hash ()]; ! ok {
469+ t .Fatalf ("rotated journal must hold the replacement: %v" , replacement .Hash ())
470+ }
471+ }
472+
473+ func TestJournalLoadDropsReplacedTransaction (t * testing.T ) {
474+ journalPath , _ , replacement := writeReplacementJournal (t , false )
475+ env := newTestEnv (t , 10 , 0 , journalPath )
476+ defer env .close ()
477+
478+ if err := env .tracker .Start (); err != nil {
479+ t .Fatalf ("Failed to start tracker: %v" , err )
480+ }
481+ defer env .tracker .Stop ()
482+
483+ env .tracker .mu .Lock ()
484+ defer env .tracker .mu .Unlock ()
485+
486+ if len (env .tracker .all ) != 1 {
487+ t .Fatalf ("loading must leave a single transaction, got %d" , len (env .tracker .all ))
488+ }
489+ if _ , ok := env .tracker .all [replacement .Hash ()]; ! ok {
490+ t .Fatalf ("the replacement must survive the load: %v" , replacement .Hash ())
491+ }
492+ }
493+
494+ // TestJournalLoadKeepsLastEntryPerNonce pins the load path for the reverse file
495+ // order. A journal entry carries no timestamp or sequence number, so when a
496+ // journal written by an older version holds both a transaction and the one that
497+ // replaced it, the entry later in the file wins the nonce -- even when that is
498+ // the transaction the user replaced. TrackAll drops the loser from the tracked
499+ // set either way; it cannot tell which one is newer.
500+ func TestJournalLoadKeepsLastEntryPerNonce (t * testing.T ) {
501+ journalPath , replaced , _ := writeReplacementJournal (t , true )
502+ env := newTestEnv (t , 10 , 0 , journalPath )
503+ defer env .close ()
504+
505+ if err := env .tracker .Start (); err != nil {
506+ t .Fatalf ("Failed to start tracker: %v" , err )
507+ }
508+ defer env .tracker .Stop ()
509+
510+ env .tracker .mu .Lock ()
511+ defer env .tracker .mu .Unlock ()
512+
513+ if len (env .tracker .all ) != 1 {
514+ t .Fatalf ("loading must leave a single transaction, got %d" , len (env .tracker .all ))
515+ }
516+ if _ , ok := env .tracker .all [replaced .Hash ()]; ! ok {
517+ t .Fatalf ("the entry later in the file must survive the load: %v" , replaced .Hash ())
518+ }
519+ }
0 commit comments