Skip to content

Commit 3b2e18a

Browse files
committed
Keeper soak test
1 parent 6da969b commit 3b2e18a

File tree

5 files changed

+466
-0
lines changed

5 files changed

+466
-0
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ test_ocr: ## run ocr tests
6868
test_ocr_soak: ## run OCR soak test
6969
NETWORK="ethereum_geth_performance" ginkgo -r --focus="@soak-ocr"
7070

71+
.PHONY: test_keeper_soak
72+
test_keeper_soak: ## run Keeper soak/performance test
73+
NETWORK="ethereum_geth_performance" ginkgo -r --focus="@performance-keeper"
74+
7175
.PHONY: test_runlog
7276
test_runlog: ## run runlog tests
7377
ginkgo -r --focus=@runlog

config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,4 @@ contracts:
233233
ocr:
234234
path: ocr/artifacts/contract/src
235235
commit: f27c14a905c5735abbb6e0c9699e9d0e3e9b7217
236+

contracts/ethereum_contracts.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,63 @@ func (o *OffchainAggregatorRoundConfirmer) Wait() error {
721721
}
722722
}
723723

724+
// KeeperConsumerRoundConfirmer is a header subscription that awaits for a round of upkeeps
725+
type KeeperConsumerRoundConfirmer struct {
726+
instance KeeperConsumer
727+
upkeepsValue int
728+
doneChan chan struct{}
729+
context context.Context
730+
cancel context.CancelFunc
731+
}
732+
733+
// NewKeeperConsumerRoundConfirmer provides a new instance of a KeeperConsumerRoundConfirmer
734+
func NewKeeperConsumerRoundConfirmer(
735+
contract KeeperConsumer,
736+
counterValue int,
737+
timeout time.Duration,
738+
) *KeeperConsumerRoundConfirmer {
739+
ctx, ctxCancel := context.WithTimeout(context.Background(), timeout)
740+
return &KeeperConsumerRoundConfirmer{
741+
instance: contract,
742+
upkeepsValue: counterValue,
743+
doneChan: make(chan struct{}),
744+
context: ctx,
745+
cancel: ctxCancel,
746+
}
747+
}
748+
749+
// ReceiveBlock will query the latest Keeper round and check to see whether the round has confirmed
750+
func (o *KeeperConsumerRoundConfirmer) ReceiveBlock(_ client.NodeBlock) error {
751+
upkeeps, err := o.instance.Counter(context.Background())
752+
if err != nil {
753+
return err
754+
}
755+
l := log.Info().
756+
Str("Contract Address", o.instance.Address()).
757+
Int64("Upkeeps", upkeeps.Int64()).
758+
Int("Required upkeeps", o.upkeepsValue)
759+
if upkeeps.Int64() == int64(o.upkeepsValue) {
760+
l.Msg("Upkeep completed")
761+
o.doneChan <- struct{}{}
762+
} else {
763+
l.Msg("Waiting for upkeep round")
764+
}
765+
return nil
766+
}
767+
768+
// Wait is a blocking function that will wait until the round has confirmed, and timeout if the deadline has passed
769+
func (o *KeeperConsumerRoundConfirmer) Wait() error {
770+
for {
771+
select {
772+
case <-o.doneChan:
773+
o.cancel()
774+
return nil
775+
case <-o.context.Done():
776+
return fmt.Errorf("timeout waiting for upkeeps to confirm: %d", o.upkeepsValue)
777+
}
778+
}
779+
}
780+
724781
// EthereumStorage acts as a conduit for the ethereum version of the storage contract
725782
type EthereumStorage struct {
726783
client *client.EthereumClient

suite/performance/keeper.go

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package performance
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/ethereum/go-ethereum/common"
7+
"github.com/onsi/ginkgo"
8+
"github.com/rs/zerolog/log"
9+
uuid "github.com/satori/go.uuid"
10+
"github.com/smartcontractkit/integrations-framework/actions"
11+
"github.com/smartcontractkit/integrations-framework/client"
12+
"github.com/smartcontractkit/integrations-framework/contracts"
13+
"github.com/smartcontractkit/integrations-framework/environment"
14+
"golang.org/x/sync/errgroup"
15+
"math/big"
16+
"time"
17+
)
18+
19+
// KeeperJobMap is a custom map type that holds the record of jobs by the contract instance and the chainlink node
20+
type KeeperJobMap map[contracts.KeeperConsumer]map[client.Chainlink]string
21+
22+
// KeeperTestOptions contains the parameters for the Keeper performance/soak test to be executed
23+
type KeeperTestOptions struct {
24+
TestOptions
25+
PaymentPremiumPPB uint32
26+
RegistryCheckGasLimit uint32
27+
BlockCountPerTurn *big.Int
28+
StalenessSeconds *big.Int
29+
GasCeilingMultiplier uint16
30+
RoundTimeout time.Duration
31+
TestDuration time.Duration
32+
}
33+
34+
// KeeperTest is the implementation of Test that will configure and execute soak test
35+
// of Keeper contracts & jobs
36+
type KeeperTest struct {
37+
TestOptions KeeperTestOptions
38+
Environment environment.Environment
39+
Blockchain client.BlockchainClient
40+
Wallets client.BlockchainWallets
41+
Deployer contracts.ContractDeployer
42+
// common contracts
43+
Link contracts.LinkToken
44+
GasFeed contracts.MockGasFeed
45+
LinkFeed contracts.MockETHLINKFeed
46+
Registry contracts.KeeperRegistry
47+
Registrar contracts.UpkeepRegistrar
48+
49+
chainlinkClients []client.Chainlink
50+
nodeAddresses []common.Address
51+
contractInstances []contracts.KeeperConsumer
52+
adapter environment.ExternalAdapter
53+
54+
jobMap KeeperJobMap
55+
}
56+
57+
// NewKeeperTest creates new Keeper performance/soak test
58+
func NewKeeperTest(
59+
testOptions KeeperTestOptions,
60+
env environment.Environment,
61+
blockchain client.BlockchainClient,
62+
wallets client.BlockchainWallets,
63+
deployer contracts.ContractDeployer,
64+
adapter environment.ExternalAdapter,
65+
link contracts.LinkToken,
66+
) Test {
67+
return &KeeperTest{
68+
TestOptions: testOptions,
69+
Environment: env,
70+
Blockchain: blockchain,
71+
Wallets: wallets,
72+
Deployer: deployer,
73+
adapter: adapter,
74+
jobMap: KeeperJobMap{},
75+
Link: link,
76+
}
77+
}
78+
79+
// RecordValues records Keeper metrics
80+
func (f *KeeperTest) RecordValues(b ginkgo.Benchmarker) error {
81+
// TODO: collect metrics
82+
return nil
83+
}
84+
85+
// Setup setups Keeper performance/soak test
86+
func (f *KeeperTest) Setup() error {
87+
chainlinkClients, err := environment.GetChainlinkClients(f.Environment)
88+
if err != nil {
89+
return err
90+
}
91+
nodeAddresses, err := actions.ChainlinkNodeAddresses(chainlinkClients)
92+
if err != nil {
93+
return err
94+
}
95+
adapter, err := environment.GetExternalAdapter(f.Environment)
96+
if err != nil {
97+
return err
98+
}
99+
f.chainlinkClients = chainlinkClients
100+
f.nodeAddresses = nodeAddresses
101+
f.adapter = adapter
102+
if err := f.deployMockFeeds(); err != nil {
103+
return err
104+
}
105+
if err := f.deployRegistry(); err != nil {
106+
return err
107+
}
108+
if err := f.deployRegistrar(); err != nil {
109+
return err
110+
}
111+
if err := f.deployConsumers(); err != nil {
112+
return err
113+
}
114+
if err := f.setKeepers(); err != nil {
115+
return err
116+
}
117+
return f.fundRegistrarBalances()
118+
}
119+
120+
func (f *KeeperTest) deployConsumerContract(c chan<- contracts.KeeperConsumer) error {
121+
consumer, err := f.Deployer.DeployKeeperConsumer(f.Wallets.Default(), big.NewInt(5))
122+
if err != nil {
123+
return err
124+
}
125+
if err = consumer.Fund(f.Wallets.Default(), big.NewFloat(0), big.NewFloat(1)); err != nil {
126+
return err
127+
}
128+
c <- consumer
129+
return nil
130+
}
131+
132+
// deployMockFeeds deploys mock ETH/Link & Gas price feeds
133+
func (f *KeeperTest) deployMockFeeds() error {
134+
var err error
135+
if f.LinkFeed, err = f.Deployer.DeployMockETHLINKFeed(f.Wallets.Default(), big.NewInt(2e18)); err != nil {
136+
return err
137+
}
138+
if f.GasFeed, err = f.Deployer.DeployMockGasFeed(f.Wallets.Default(), big.NewInt(2e11)); err != nil {
139+
return err
140+
}
141+
return f.Blockchain.WaitForEvents()
142+
}
143+
144+
// deployRegistrar deploys contract for registering upkeeps
145+
func (f *KeeperTest) deployRegistrar() error {
146+
var err error
147+
f.Registrar, err = f.Deployer.DeployUpkeepRegistrationRequests(
148+
f.Wallets.Default(),
149+
f.Link.Address(),
150+
big.NewInt(0),
151+
)
152+
if err != nil {
153+
return err
154+
}
155+
if err = f.Registry.SetRegistrar(f.Wallets.Default(), f.Registrar.Address()); err != nil {
156+
return err
157+
}
158+
if err = f.Registrar.SetRegistrarConfig(
159+
f.Wallets.Default(),
160+
true,
161+
uint32(999),
162+
uint16(999),
163+
f.Registry.Address(),
164+
big.NewInt(0),
165+
); err != nil {
166+
return err
167+
}
168+
return f.Blockchain.WaitForEvents()
169+
}
170+
171+
// deployRegistry deploys keeper registry
172+
func (f *KeeperTest) deployRegistry() error {
173+
var err error
174+
f.Registry, err = f.Deployer.DeployKeeperRegistry(
175+
f.Wallets.Default(),
176+
&contracts.KeeperRegistryOpts{
177+
LinkAddr: f.Link.Address(),
178+
ETHFeedAddr: f.LinkFeed.Address(),
179+
GasFeedAddr: f.GasFeed.Address(),
180+
PaymentPremiumPPB: f.TestOptions.PaymentPremiumPPB,
181+
BlockCountPerTurn: f.TestOptions.BlockCountPerTurn,
182+
CheckGasLimit: f.TestOptions.RegistryCheckGasLimit,
183+
StalenessSeconds: f.TestOptions.StalenessSeconds,
184+
GasCeilingMultiplier: f.TestOptions.GasCeilingMultiplier,
185+
FallbackGasPrice: big.NewInt(2e11),
186+
FallbackLinkPrice: big.NewInt(2e18),
187+
},
188+
)
189+
if err != nil {
190+
return err
191+
}
192+
if err = f.Registry.Fund(f.Wallets.Default(), big.NewFloat(0), big.NewFloat(1)); err != nil {
193+
return err
194+
}
195+
return nil
196+
}
197+
198+
// setKeepers sets keepers, all keepers are "payees" too
199+
func (f *KeeperTest) setKeepers() error {
200+
nodeAddresses := make([]string, 0)
201+
for _, node := range f.chainlinkClients {
202+
nodeAddr, err := node.PrimaryEthAddress()
203+
if err != nil {
204+
return err
205+
}
206+
nodeAddresses = append(nodeAddresses, nodeAddr)
207+
}
208+
if err := f.Registry.SetKeepers(f.Wallets.Default(), nodeAddresses, nodeAddresses); err != nil {
209+
return err
210+
}
211+
return f.Blockchain.WaitForEvents()
212+
}
213+
214+
//fundRegistrarBalances funds registrar balances so payees can be charged
215+
func (f *KeeperTest) fundRegistrarBalances() error {
216+
g := errgroup.Group{}
217+
for _, consumer := range f.contractInstances {
218+
consumer := consumer
219+
g.Go(func() error {
220+
req, err := f.Registrar.EncodeRegisterRequest(
221+
fmt.Sprintf("upkeep_perf_%s", uuid.NewV4().String()),
222+
[]byte("0x1234"),
223+
consumer.Address(),
224+
f.TestOptions.RegistryCheckGasLimit,
225+
f.Wallets.Default().Address(),
226+
[]byte("0x"),
227+
big.NewInt(9e18),
228+
0,
229+
)
230+
if err != nil {
231+
return err
232+
}
233+
if err = f.Link.TransferAndCall(f.Wallets.Default(), f.Registrar.Address(), big.NewInt(9e18), req); err != nil {
234+
return err
235+
}
236+
return nil
237+
})
238+
}
239+
if err := g.Wait(); err != nil {
240+
return err
241+
}
242+
return f.Blockchain.WaitForEvents()
243+
}
244+
245+
// deployConsumers deploys consumers on which upkeeps will be performed
246+
func (f *KeeperTest) deployConsumers() error {
247+
contractChan := make(chan contracts.KeeperConsumer, f.TestOptions.NumberOfContracts)
248+
g := errgroup.Group{}
249+
250+
for i := 0; i < f.TestOptions.NumberOfContracts; i++ {
251+
g.Go(func() error {
252+
return f.deployConsumerContract(contractChan)
253+
})
254+
}
255+
if err := g.Wait(); err != nil {
256+
return err
257+
}
258+
if err := f.Blockchain.WaitForEvents(); err != nil {
259+
return err
260+
}
261+
close(contractChan)
262+
for contract := range contractChan {
263+
f.contractInstances = append(f.contractInstances, contract)
264+
}
265+
return nil
266+
}
267+
268+
// Run runs Keeper performance/soak test
269+
func (f *KeeperTest) Run() error {
270+
if err := f.createKeeperJobs(); err != nil {
271+
return err
272+
}
273+
ctx, cancel := context.WithTimeout(context.Background(), f.TestOptions.TestDuration)
274+
defer cancel()
275+
i := 1
276+
for {
277+
select {
278+
case <-ctx.Done():
279+
log.Warn().Msg("Test finished")
280+
return nil
281+
default:
282+
requiredUpkeeps := i * len(f.chainlinkClients)
283+
log.Warn().Int("RoundID", i).Int("Required upkeeps", requiredUpkeeps).Msg("New round")
284+
if err := f.waitForUpkeeps(requiredUpkeeps); err != nil {
285+
return err
286+
}
287+
i++
288+
}
289+
}
290+
}
291+
292+
// waitForUpkeeps await that every consumer upkeep was called and counter inside contract is incremented for that round
293+
func (f *KeeperTest) waitForUpkeeps(upkeeps int) error {
294+
for _, consumer := range f.contractInstances {
295+
upkeepRound := contracts.NewKeeperConsumerRoundConfirmer(consumer, upkeeps, f.TestOptions.RoundTimeout)
296+
f.Blockchain.AddHeaderEventSubscription(consumer.Address(), upkeepRound)
297+
}
298+
return f.Blockchain.WaitForEvents()
299+
}
300+
301+
// createKeeperJobs creates keeper jobs
302+
func (f *KeeperTest) createKeeperJobs() error {
303+
for _, node := range f.chainlinkClients {
304+
nodeAddr, err := node.PrimaryEthAddress()
305+
if err != nil {
306+
return err
307+
}
308+
_, err = node.CreateJob(&client.KeeperJobSpec{
309+
Name: "keeper",
310+
ContractAddress: f.Registry.Address(),
311+
FromAddress: nodeAddr,
312+
})
313+
if err != nil {
314+
return err
315+
}
316+
}
317+
return nil
318+
}

0 commit comments

Comments
 (0)