Skip to content

Commit 51a0907

Browse files
committed
[4110] add unit tests for actpool/queueworker
1 parent 0694b52 commit 51a0907

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed

actpool/queueworker_test.go

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
package actpool
2+
3+
import (
4+
"context"
5+
"errors"
6+
"math/big"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/require"
11+
"go.uber.org/mock/gomock"
12+
13+
"github.com/iotexproject/go-pkgs/crypto"
14+
"github.com/iotexproject/iotex-core/v2/action"
15+
"github.com/iotexproject/iotex-core/v2/action/protocol"
16+
"github.com/iotexproject/iotex-core/v2/action/protocol/account/accountpb"
17+
accountutil "github.com/iotexproject/iotex-core/v2/action/protocol/account/util"
18+
"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
19+
"github.com/iotexproject/iotex-core/v2/state"
20+
"github.com/iotexproject/iotex-core/v2/test/identityset"
21+
"github.com/iotexproject/iotex-core/v2/test/mock/mock_chainmanager"
22+
)
23+
24+
func TestQueueWorker(t *testing.T) {
25+
r := require.New(t)
26+
ctrl := gomock.NewController(t)
27+
28+
ap, sf, err := newTestActPool(ctrl)
29+
r.NoError(err)
30+
31+
senderKey := identityset.PrivateKey(28)
32+
senderAddrStr := identityset.Address(28).String()
33+
34+
// setup sf mock to return a valid account
35+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
36+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
37+
pb := &accountpb.Account{
38+
Nonce: 1, // Confirmed nonce
39+
Balance: "10000000000",
40+
Type: accountpb.AccountType_ZERO_NONCE,
41+
}
42+
account.FromProto(pb)
43+
return 1, nil
44+
}).AnyTimes()
45+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
46+
47+
jobQueue := make(chan workerJob, 1)
48+
worker := newQueueWorker(ap, jobQueue)
49+
r.NotNil(worker)
50+
// This is a bit of a hack. The actpool allocates workers internally based on sender address.
51+
// For this test, we will replace all workers with our test worker.
52+
for i := 0; i < len(ap.worker); i++ {
53+
ap.worker[i] = worker
54+
}
55+
56+
g := genesis.TestDefault()
57+
bctx := protocol.BlockCtx{}
58+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
59+
60+
r.NoError(worker.Start())
61+
defer worker.Stop()
62+
63+
// Test Handle
64+
act, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
65+
r.NoError(err)
66+
67+
job := workerJob{
68+
ctx: ctx,
69+
act: act,
70+
err: make(chan error, 1),
71+
}
72+
73+
jobQueue <- job
74+
err = <-job.err
75+
r.NoError(err)
76+
77+
// Verify action was added
78+
pNonce, err := ap.GetPendingNonce(senderAddrStr)
79+
r.NoError(err)
80+
r.Equal(uint64(2), pNonce)
81+
}
82+
83+
func TestQueueWorker_HandleErrors(t *testing.T) {
84+
r := require.New(t)
85+
ctrl := gomock.NewController(t)
86+
87+
senderKey := identityset.PrivateKey(28)
88+
89+
baseJob := func(ctx context.Context, nonce uint64, amount *big.Int) workerJob {
90+
act, err := signedTransfer(senderKey, nonce, amount, "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
91+
r.NoError(err)
92+
return workerJob{
93+
ctx: ctx,
94+
act: act,
95+
err: make(chan error, 1),
96+
}
97+
}
98+
99+
t.Run("nonce too low", func(t *testing.T) {
100+
ap, sf, err := newTestActPool(ctrl)
101+
r.NoError(err)
102+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
103+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
104+
pb := &accountpb.Account{
105+
Nonce: 5, // Confirmed nonce
106+
Balance: "10000000000",
107+
Type: accountpb.AccountType_ZERO_NONCE,
108+
}
109+
account.FromProto(pb)
110+
return 1, nil
111+
}).AnyTimes()
112+
worker := newQueueWorker(ap, make(chan workerJob))
113+
g := genesis.TestDefault()
114+
bctx := protocol.BlockCtx{}
115+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
116+
job := baseJob(ctx, 4, big.NewInt(100)) // Action nonce is 4
117+
err = worker.Handle(job)
118+
r.ErrorIs(err, action.ErrNonceTooLow)
119+
})
120+
121+
t.Run("nonce too high", func(t *testing.T) {
122+
ap, sf, err := newTestActPool(ctrl)
123+
r.NoError(err)
124+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
125+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
126+
pb := &accountpb.Account{
127+
Nonce: 5, // Confirmed nonce
128+
Balance: "10000000000",
129+
Type: accountpb.AccountType_ZERO_NONCE,
130+
}
131+
account.FromProto(pb)
132+
return 1, nil
133+
}).AnyTimes()
134+
worker := newQueueWorker(ap, make(chan workerJob))
135+
g := genesis.TestDefault()
136+
bctx := protocol.BlockCtx{}
137+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
138+
job := baseJob(ctx, 5+ap.cfg.MaxNumActsPerAcct, big.NewInt(100))
139+
err = worker.Handle(job)
140+
r.ErrorIs(err, action.ErrNonceTooHigh)
141+
})
142+
143+
t.Run("insufficient funds", func(t *testing.T) {
144+
ap, sf, err := newTestActPool(ctrl)
145+
r.NoError(err)
146+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
147+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
148+
pb := &accountpb.Account{
149+
Nonce: 0,
150+
Balance: "100", // Low balance
151+
Type: accountpb.AccountType_ZERO_NONCE,
152+
}
153+
account.FromProto(pb)
154+
return 1, nil
155+
}).AnyTimes()
156+
worker := newQueueWorker(ap, make(chan workerJob))
157+
g := genesis.TestDefault()
158+
bctx := protocol.BlockCtx{}
159+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
160+
// Action cost is 100000 (gas) * 1 (gasprice) + 1000 (amount) = 101000
161+
job := baseJob(ctx, 1, big.NewInt(1000))
162+
err = worker.Handle(job)
163+
r.ErrorIs(err, action.ErrInsufficientFunds)
164+
})
165+
166+
t.Run("context canceled", func(t *testing.T) {
167+
ap, _, err := newTestActPool(ctrl)
168+
r.NoError(err)
169+
worker := newQueueWorker(ap, make(chan workerJob))
170+
g := genesis.TestDefault()
171+
bctx := protocol.BlockCtx{}
172+
baseCtx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
173+
ctx, cancel := context.WithCancel(baseCtx)
174+
cancel() // Cancel context immediately
175+
job := baseJob(ctx, 1, big.NewInt(100))
176+
err = worker.Handle(job)
177+
r.ErrorIs(err, context.Canceled)
178+
})
179+
}
180+
181+
func TestQueueWorker_StateError(t *testing.T) {
182+
r := require.New(t)
183+
ctrl := gomock.NewController(t)
184+
stateErr := errors.New("state error")
185+
186+
t.Run("handle state error", func(t *testing.T) {
187+
ap, sf, err := newTestActPool(ctrl)
188+
r.NoError(err)
189+
sf.EXPECT().State(gomock.Any(), gomock.Any()).Return(uint64(0), stateErr).Times(1)
190+
worker := newQueueWorker(ap, make(chan workerJob))
191+
192+
senderKey := identityset.PrivateKey(28)
193+
act, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
194+
r.NoError(err)
195+
196+
g := genesis.TestDefault()
197+
bctx := protocol.BlockCtx{}
198+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
199+
200+
job := workerJob{ctx: ctx, act: act}
201+
err = worker.Handle(job)
202+
r.ErrorIs(err, stateErr)
203+
})
204+
205+
t.Run("reset state error", func(t *testing.T) {
206+
ap, sf, err := newTestActPool(ctrl)
207+
r.NoError(err)
208+
209+
senderKey := identityset.PrivateKey(28)
210+
senderAddrStr := identityset.Address(28).String()
211+
act, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
212+
r.NoError(err)
213+
214+
// Mock a successful state read for the initial add
215+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
216+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
217+
pb := &accountpb.Account{
218+
Nonce: 1,
219+
Balance: "10000000000",
220+
Type: accountpb.AccountType_ZERO_NONCE,
221+
}
222+
account.FromProto(pb)
223+
return 1, nil
224+
}).Times(1)
225+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
226+
227+
worker := newQueueWorker(ap, make(chan workerJob))
228+
g := genesis.TestDefault()
229+
bctx := protocol.BlockCtx{}
230+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
231+
err = worker.Handle(workerJob{ctx: ctx, act: act})
232+
r.NoError(err)
233+
r.False(worker.accountActs.Account(senderAddrStr).Empty())
234+
235+
// Mock a failed state read for the reset
236+
sf.EXPECT().State(gomock.Any(), gomock.Any()).Return(uint64(0), stateErr).Times(1)
237+
worker.Reset(ctx)
238+
r.True(worker.accountActs.Account(senderAddrStr).Empty())
239+
})
240+
}
241+
242+
func TestQueueWorker_EdgeCases(t *testing.T) {
243+
r := require.New(t)
244+
ctrl := gomock.NewController(t)
245+
246+
t.Run("replace with overflow", func(t *testing.T) {
247+
ap, sf, err := newTestActPool(ctrl)
248+
r.NoError(err)
249+
ap.cfg.MaxNumActsPerPool = 1 // Set pool size to 1
250+
251+
senderKey := identityset.PrivateKey(28)
252+
act1, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
253+
r.NoError(err)
254+
act2, err := signedTransfer(senderKey, 1, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(2))
255+
r.NoError(err)
256+
257+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
258+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
259+
pb := &accountpb.Account{
260+
Nonce: 1,
261+
Balance: "10000000000",
262+
Type: accountpb.AccountType_ZERO_NONCE,
263+
}
264+
account.FromProto(pb)
265+
return 1, nil
266+
}).AnyTimes()
267+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
268+
269+
worker := newQueueWorker(ap, make(chan workerJob))
270+
g := genesis.TestDefault()
271+
bctx := protocol.BlockCtx{}
272+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
273+
274+
// Add first action
275+
err = worker.Handle(workerJob{ctx: ctx, act: act1})
276+
r.NoError(err)
277+
278+
// Replace with second action, causing overflow
279+
job := workerJob{ctx: ctx, act: act2, rep: true}
280+
err = worker.Handle(job)
281+
r.ErrorIs(err, action.ErrTxPoolOverflow)
282+
})
283+
284+
t.Run("pending actions with timeout", func(t *testing.T) {
285+
ap, sf, err := newTestActPool(ctrl)
286+
r.NoError(err)
287+
ap.cfg.ActionExpiry = 1 * time.Millisecond // Set a short expiry
288+
289+
senderKey := identityset.PrivateKey(28)
290+
// Action with nonce 2, which is greater than the pending nonce 1
291+
act, err := signedTransfer(senderKey, 2, big.NewInt(100), "io1mflp9m6hcgm2qcghchsdqj3z3adcdw2h72f73h", nil, 100000, big.NewInt(1))
292+
r.NoError(err)
293+
294+
sf.EXPECT().State(gomock.Any(), gomock.Any()).DoAndReturn(
295+
func(account *state.Account, opts ...protocol.StateOption) (uint64, error) {
296+
pb := &accountpb.Account{
297+
Nonce: 1,
298+
Balance: "10000000000",
299+
Type: accountpb.AccountType_ZERO_NONCE,
300+
}
301+
account.FromProto(pb)
302+
return 1, nil
303+
}).AnyTimes()
304+
sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes()
305+
306+
worker := newQueueWorker(ap, make(chan workerJob))
307+
g := genesis.TestDefault()
308+
bctx := protocol.BlockCtx{}
309+
ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), g), bctx))
310+
311+
err = worker.Handle(workerJob{ctx: ctx, act: act})
312+
r.NoError(err)
313+
314+
time.Sleep(2 * time.Millisecond) // Wait for action to expire
315+
316+
pending := worker.PendingActions(ctx)
317+
r.Empty(pending)
318+
})
319+
320+
t.Run("all actions for non-existent account", func(t *testing.T) {
321+
ap, _, err := newTestActPool(ctrl)
322+
r.NoError(err)
323+
worker := newQueueWorker(ap, make(chan workerJob))
324+
addr := identityset.Address(29)
325+
acts, ok := worker.AllActions(addr)
326+
r.False(ok)
327+
r.Nil(acts)
328+
})
329+
330+
t.Run("pending nonce for non-existent account", func(t *testing.T) {
331+
ap, _, err := newTestActPool(ctrl)
332+
r.NoError(err)
333+
worker := newQueueWorker(ap, make(chan workerJob))
334+
addr := identityset.Address(29)
335+
nonce, ok := worker.PendingNonce(addr)
336+
r.False(ok)
337+
r.Zero(nonce)
338+
})
339+
}
340+
341+
func signedTransfer(
342+
senderKey crypto.PrivateKey,
343+
nonce uint64,
344+
amount *big.Int,
345+
recipient string,
346+
payload []byte,
347+
gasLimit uint64,
348+
gasPrice *big.Int,
349+
) (*action.SealedEnvelope, error) {
350+
transfer := action.NewTransfer(amount, recipient, payload)
351+
bd := &action.EnvelopeBuilder{}
352+
elp := bd.SetNonce(nonce).
353+
SetGasLimit(gasLimit).
354+
SetGasPrice(gasPrice).
355+
SetAction(transfer).
356+
Build()
357+
return action.Sign(elp, senderKey)
358+
}
359+
360+
func newTestActPool(ctrl *gomock.Controller) (*actPool, *mock_chainmanager.MockStateReader, error) {
361+
cfg := DefaultConfig
362+
cfg.ActionExpiry = 10 * time.Second
363+
sf := mock_chainmanager.NewMockStateReader(ctrl)
364+
ap, err := NewActPool(genesis.TestDefault(), sf, cfg)
365+
if err != nil {
366+
return nil, nil, err
367+
}
368+
ap.AddActionEnvelopeValidators(protocol.NewGenericValidator(sf, accountutil.AccountState))
369+
actPool, ok := ap.(*actPool)
370+
if !ok {
371+
panic("wrong type")
372+
}
373+
return actPool, sf, nil
374+
}

0 commit comments

Comments
 (0)