Skip to content

Commit 6ddb724

Browse files
committed
feat(core/txpool,eth): track already-known local transactions on resubmit
A local transaction that reaches the pool through a concurrent submission (peer gossip, wallet retry, or a parallel RPC endpoint) returns txpool.ErrAlreadyKnown from TxPool.Add, but is not in the desired local tracking state: the local tracker only starts on a successful admission. If that transaction is later evicted, it has no local resubmit or journal protection and silently disappears. Treat ErrAlreadyKnown as the desired state on the initial admission path: AddLocal and EthAPIBackend.SendTx still surface the error to the caller (matching upstream go-ethereum semantics), but now also register the transaction with the local tracker so it keeps the resubmit and journal guarantees. The resubmit loop (TxTracker.loop -> TxTracker.recheck) already retains already-known transactions in the tracked set, because recheck skips transactions still present in the pool (pool.Has), so the two paths are now consistent.
1 parent 1daf46f commit 6ddb724

4 files changed

Lines changed: 72 additions & 11 deletions

File tree

core/txpool/txpool.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,13 +364,16 @@ func (p *TxPool) SetLocalTracker(tracker LocalTracker) {
364364
}
365365

366366
// AddLocal enqueues a single local transaction into the pool and return the
367-
// original error. The transaction will be tracked if it was accepted or
368-
// rejected for a temporary reason, allowing the local tracker to implement
369-
// re-journal and re-submit flows.
367+
// original error. The transaction will be tracked if it was accepted, already
368+
// known to the pool, or rejected for a temporary reason, allowing the local
369+
// tracker to implement re-journal and re-submit flows.
370370
func (p *TxPool) AddLocal(tx *types.Transaction, sync bool) error {
371371
err := p.Add([]*types.Transaction{tx}, sync)[0]
372372
if p.localTracker != nil {
373-
if err == nil || p.localTracker.IsRetryableReject(err) {
373+
// An already-known transaction is in the desired state: it lost a race
374+
// to a concurrent submission of the same transaction, so track it to
375+
// keep the local resubmit protection, but still surface the error.
376+
if err == nil || p.localTracker.IsRetryableReject(err) || errors.Is(err, ErrAlreadyKnown) {
374377
p.localTracker.Track(tx)
375378
}
376379
}

core/txpool/txpool_local_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,38 @@ func TestAddLocalTracksTemporaryRejectedTransaction(t *testing.T) {
283283
}
284284
}
285285

286+
func TestAddLocalTracksAlreadyKnownTransaction(t *testing.T) {
287+
events := []string{}
288+
tracker := &testLocalTracker{events: &events}
289+
subpool := &testSubPool{
290+
events: &events,
291+
addErrs: []error{ErrAlreadyKnown},
292+
}
293+
294+
pool, err := New(0, testChain{}, []SubPool{subpool})
295+
if err != nil {
296+
t.Fatalf("failed to create txpool: %v", err)
297+
}
298+
defer pool.Close()
299+
300+
pool.SetLocalTracker(tracker)
301+
302+
tx := types.NewTransaction(0, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
303+
err = pool.AddLocal(tx, true)
304+
if !errors.Is(err, ErrAlreadyKnown) {
305+
t.Fatalf("unexpected error: have %v, want %v", err, ErrAlreadyKnown)
306+
}
307+
308+
// The transaction is in the desired state (already in the pool), so it
309+
// must still be tracked for the local resubmit flow.
310+
if !reflect.DeepEqual(tracker.tracked, []common.Hash{tx.Hash()}) {
311+
t.Fatalf("tracker should receive already-known local tx")
312+
}
313+
if !reflect.DeepEqual(events, []string{"add", "track"}) {
314+
t.Fatalf("unexpected call order: have %v", events)
315+
}
316+
}
317+
286318
func TestAddLocalTemporaryRejectWithoutTrackerReturnsError(t *testing.T) {
287319
events := []string{}
288320
subpool := &testSubPool{

eth/api_backend.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,16 +318,17 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction)
318318
if b.eth.localTxTracker == nil {
319319
return err
320320
}
321-
// If the transaction fails with an error indicating it is invalid, or if there is
322-
// very little chance it will be accepted later (e.g., the gas price is below the
323-
// configured minimum, or the sender has insufficient funds to cover the cost),
324-
// propagate the error to the user.
321+
// Track the transaction unless it was permanently rejected. A transaction
322+
// is tracked when it is accepted, temporarily rejected, or already known
323+
// to the pool. An already-known transaction is in the desired state (it
324+
// lost a race to a concurrent submission), so we still track it, but the
325+
// error is surfaced to the caller to match upstream go-ethereum semantics.
325326
if err != nil && !locals.IsTemporaryReject(err) {
327+
if errors.Is(err, txpool.ErrAlreadyKnown) {
328+
b.eth.localTxTracker.Track(signedTx)
329+
}
326330
return err
327331
}
328-
// No error will be returned to user if the transaction fails with a temporary
329-
// error and might be accepted later (e.g., the transaction pool is full).
330-
// Locally submitted transactions will be resubmitted later via the local tracker.
331332
b.eth.localTxTracker.Track(signedTx)
332333
return nil
333334
}

eth/api_backend_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,28 @@ func TestSendTxWithLocalPermanentErrorNotTracked(t *testing.T) {
230230
t.Fatalf("unexpected tracked tx count: have %d, want 0", tracked)
231231
}
232232
}
233+
234+
func TestSendTxTracksAlreadyKnown(t *testing.T) {
235+
b := initBackend(t, true)
236+
if b.eth.localTxTracker == nil {
237+
t.Fatal("expected local tx tracker to be configured")
238+
}
239+
tx := makeTx(0, nil, nil, key)
240+
// Simulate the transaction reaching the pool via gossip: a plain pool add
241+
// does not involve the local tracker.
242+
if err := b.eth.txPool.Add([]*types.Transaction{tx}, true)[0]; err != nil {
243+
t.Fatalf("failed to seed the pool with the transaction: %v", err)
244+
}
245+
if tracked := reflect.ValueOf(b.eth.localTxTracker).Elem().FieldByName("all").Len(); tracked != 0 {
246+
t.Fatalf("unexpected tracked tx count before resubmission: have %d, want 0", tracked)
247+
}
248+
// Submitting the same transaction locally must report ErrAlreadyKnown to
249+
// the submitter while still tracking it for the local resubmit flow.
250+
err := b.SendTx(context.Background(), tx)
251+
if !errors.Is(err, txpool.ErrAlreadyKnown) {
252+
t.Fatalf("unexpected error, want: %v, got: %v", txpool.ErrAlreadyKnown, err)
253+
}
254+
if tracked := reflect.ValueOf(b.eth.localTxTracker).Elem().FieldByName("all").Len(); tracked != 1 {
255+
t.Fatalf("unexpected tracked tx count: have %d, want 1", tracked)
256+
}
257+
}

0 commit comments

Comments
 (0)