Skip to content

Commit ecad231

Browse files
authored
Merge pull request #12 from flare-foundation/staking-clients
Test for uptime voting cronjob
2 parents 6090c08 + 43289fd commit ecad231

20 files changed

Lines changed: 1449 additions & 890 deletions

database/testing.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,9 @@ func FetchUptimes(db *gorm.DB, nodeIDs []string, start time.Time, end time.Time)
8383
err := query.Find(&uptimes).Error
8484
return uptimes, err
8585
}
86+
87+
func FetchAggregations(db *gorm.DB) ([]*UptimeAggregation, error) {
88+
var aggregations []*UptimeAggregation
89+
err := db.Find(&aggregations).Error
90+
return aggregations, err
91+
}

indexer/config/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ type VotingConfig struct {
4949
}
5050

5151
type EpochConfig struct {
52-
Period time.Duration `toml:"epoch_period" envconfig:"EPOCH_PERIOD"`
53-
Start utils.Timestamp `toml:"epoch_time" envconfig:"EPOCH_TIME"`
52+
Period time.Duration `toml:"period" envconfig:"EPOCH_PERIOD"`
53+
Start utils.Timestamp `toml:"start" envconfig:"EPOCH_TIME"`
5454
}
5555

5656
type UptimeConfig struct {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
([]string) (len=4) {
2+
(string) (len=40) "NodeID-GWPcbFJZFfZreETSoWjPimr846mXEKCtu",
3+
(string) (len=40) "NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ",
4+
(string) (len=40) "NodeID-NFBbbJ4qCmNaCzeW7sxErhvWqvEQMnYcN",
5+
(string) (len=40) "NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5"
6+
}
7+
([]int64) (len=4) {
8+
(int64) 90,
9+
(int64) 90,
10+
(int64) 90,
11+
(int64) 40
12+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
([32]uint8) (len=32) {
2+
00000000 2b 2e 37 55 43 1e 55 53 ee 31 d3 cd 2d 5c 57 20 |+.7UC.US.1..-\W |
3+
00000010 31 1d aa f5 c7 d2 a2 fd 43 07 da bf c3 44 07 c4 |1.......C....D..|
4+
}

indexer/cronjob/main_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import (
66
"testing"
77
)
88

9+
const (
10+
privateKey1 = "0xd49743deccbccc5dc7baa8e69e5be03298da8688a15dd202e20f15d5e0e9a9fb"
11+
privateKey2 = "0x23c601ae397441f3ef6f1075dcb0031ff17fb079837beadaf3c84d96c6f3e569"
12+
)
13+
914
var (
15+
testClient *chain.RecordedIndexerClient //:= chain.PChainTestClient(t)
16+
testRPCClient *chain.RecordedRPCClient //:= chain.PChainTestRPCClient(t)
1017
testUptimeClient *chain.RecordedUptimeClient
1118
)
1219

@@ -16,5 +23,14 @@ func TestMain(m *testing.M) {
1623
if err != nil {
1724
log.Fatal(err)
1825
}
26+
testClient, err = chain.PChainTestClient()
27+
if err != nil {
28+
log.Fatal(err)
29+
}
30+
31+
testRPCClient, err = chain.PChainTestRPCClient()
32+
if err != nil {
33+
log.Fatal(err)
34+
}
1935
m.Run()
2036
}

indexer/cronjob/mirror.go

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cronjob
22

33
import (
4-
"context"
54
"flare-indexer/database"
65
"flare-indexer/indexer/config"
76
indexerctx "flare-indexer/indexer/context"
@@ -16,7 +15,6 @@ import (
1615
"github.com/ethereum/go-ethereum/common"
1716
"github.com/ethereum/go-ethereum/ethclient"
1817
"github.com/pkg/errors"
19-
"golang.org/x/sync/errgroup"
2018
"gorm.io/gorm"
2119
)
2220

@@ -179,47 +177,23 @@ type epochRange struct {
179177

180178
var errNoEpochsToMirror = errors.New("no epochs to mirror")
181179

182-
func (e *epochRange) validate() error {
183-
if e.start > e.end {
184-
return errNoEpochsToMirror
185-
}
186-
187-
return nil
188-
}
189-
190180
func (c *mirrorCronJob) getEpochRange() (*epochRange, error) {
191-
epochRange := new(epochRange)
192-
eg, ctx := errgroup.WithContext(context.Background())
193-
194-
eg.Go(func() error {
195-
startEpoch, err := c.getStartEpoch()
196-
if err != nil {
197-
return err
198-
}
199-
200-
epochRange.start = startEpoch
201-
return nil
202-
})
203-
204-
eg.Go(func() error {
205-
endEpoch, err := c.getEndEpoch(ctx)
206-
if err != nil {
207-
return err
208-
}
209-
210-
epochRange.end = endEpoch
211-
return nil
212-
})
213-
214-
if err := eg.Wait(); err != nil {
181+
startEpoch, err := c.getStartEpoch()
182+
if err != nil {
215183
return nil, err
216184
}
217185

218-
if err := epochRange.validate(); err != nil {
186+
logger.Debug("start epoch: %d", startEpoch)
187+
188+
endEpoch, err := c.getEndEpoch(startEpoch)
189+
if err != nil {
219190
return nil, err
220191
}
221192

222-
return epochRange, nil
193+
return &epochRange{
194+
start: startEpoch,
195+
end: endEpoch,
196+
}, nil
223197
}
224198

225199
func (c *mirrorCronJob) getStartEpoch() (int64, error) {
@@ -231,11 +205,12 @@ func (c *mirrorCronJob) getStartEpoch() (int64, error) {
231205
return int64(jobState.NextDBIndex), nil
232206
}
233207

234-
func (c *mirrorCronJob) getEndEpoch(ctx context.Context) (int64, error) {
208+
func (c *mirrorCronJob) getEndEpoch(startEpoch int64) (int64, error) {
235209
currEpoch := c.epochs.getCurrentEpoch()
210+
logger.Debug("current epoch: %d", currEpoch)
236211

237-
for epoch := currEpoch; epoch > 0; epoch-- {
238-
confirmed, err := c.isEpochConfirmed(ctx, epoch)
212+
for epoch := currEpoch; epoch > startEpoch; epoch-- {
213+
confirmed, err := c.isEpochConfirmed(epoch)
239214
if err != nil {
240215
return 0, err
241216
}
@@ -248,9 +223,8 @@ func (c *mirrorCronJob) getEndEpoch(ctx context.Context) (int64, error) {
248223
return 0, errNoEpochsToMirror
249224
}
250225

251-
func (c *mirrorCronJob) isEpochConfirmed(ctx context.Context, epoch int64) (bool, error) {
252-
opts := &bind.CallOpts{Context: ctx}
253-
merkleRoot, err := c.votingContract.GetMerkleRoot(opts, big.NewInt(epoch))
226+
func (c *mirrorCronJob) isEpochConfirmed(epoch int64) (bool, error) {
227+
merkleRoot, err := c.votingContract.GetMerkleRoot(new(bind.CallOpts), big.NewInt(epoch))
254228
if err != nil {
255229
return false, errors.Wrap(err, "votingContract.GetMerkleRoot")
256230
}
@@ -299,6 +273,10 @@ func (c *mirrorCronJob) mirrorTxs(txs []database.PChainTxData, epochID int64) er
299273
return err
300274
}
301275

276+
if err := c.checkMerkleRoot(merkleTree, epochID); err != nil {
277+
return err
278+
}
279+
302280
for i := range txs {
303281
in := mirrorTxInput{
304282
epochID: big.NewInt(epochID),
@@ -314,6 +292,24 @@ func (c *mirrorCronJob) mirrorTxs(txs []database.PChainTxData, epochID int64) er
314292
return nil
315293
}
316294

295+
func (c *mirrorCronJob) checkMerkleRoot(tree merkle.Tree, epoch int64) error {
296+
root, err := tree.Root()
297+
if err != nil {
298+
return err
299+
}
300+
301+
contractRoot, err := c.votingContract.GetMerkleRoot(new(bind.CallOpts), big.NewInt(epoch))
302+
if err != nil {
303+
return errors.Wrap(err, "votingContract.GetMerkleRoot")
304+
}
305+
306+
if root != contractRoot {
307+
return errors.Errorf("merkle root mismatch: got %x, expected %x", root, contractRoot)
308+
}
309+
310+
return nil
311+
}
312+
317313
type mirrorTxInput struct {
318314
epochID *big.Int
319315
merkleTree merkle.Tree
@@ -331,7 +327,7 @@ func (c *mirrorCronJob) mirrorTx(in *mirrorTxInput) error {
331327
return err
332328
}
333329

334-
_, err = c.mirroringContract.VerifyStake(c.txOpts, *stakeData, merkleProof)
330+
_, err = c.mirroringContract.MirrorStake(c.txOpts, *stakeData, merkleProof)
335331
if err != nil {
336332
return errors.Wrap(err, "mirroringContract.VerifyStake")
337333
}

indexer/cronjob/uptime_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"flare-indexer/database"
66
"flare-indexer/indexer/config"
77
"flare-indexer/indexer/context"
8-
"flare-indexer/utils"
98
"testing"
109
"time"
1110
)
@@ -53,7 +52,7 @@ func TestUptime(t *testing.T) {
5352
t.Fatal(err)
5453
}
5554

56-
now := utils.ParseTime("2023-02-02T14:29:50Z")
55+
now := time.Unix(1675348249, 0)
5756
testUptimeClient.SetNow(now)
5857

5958
for i := 0; i < 100; i++ {
@@ -67,7 +66,7 @@ func TestUptime(t *testing.T) {
6766
if err != nil {
6867
t.Fatal(err)
6968
}
70-
if len(uptimes) != 6 {
71-
t.Fatalf("expected 6 uptimes, got %d", len(uptimes))
69+
if len(uptimes) != 8 {
70+
t.Fatalf("expected 8 uptimes, got %d", len(uptimes))
7271
}
7372
}

indexer/cronjob/uptime_voting.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ type uptimeVotingCronjob struct {
4343
txOpts *bind.TransactOpts
4444

4545
db *gorm.DB
46+
47+
// For testing to set "now" to some past date
48+
time utils.ShiftedTime
4649
}
4750

48-
func NewUptimeVotingCronjob(ctx context.IndexerContext) (Cronjob, error) {
51+
func NewUptimeVotingCronjob(ctx context.IndexerContext) (*uptimeVotingCronjob, error) {
4952
cfg := ctx.Config()
5053

5154
if !cfg.UptimeCronjob.EnableVoting {
@@ -93,7 +96,7 @@ func (c *uptimeVotingCronjob) OnStart() error {
9396
}
9497

9598
func (c *uptimeVotingCronjob) Call() error {
96-
now := time.Now()
99+
now := c.time.Now()
97100
firstEpochToAggregate, lastEpochToAggregate, err := c.aggregationRange(now)
98101
if err != nil {
99102
if err == errNoEpochsToAggregate {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package cronjob
2+
3+
import (
4+
globalConfig "flare-indexer/config"
5+
"flare-indexer/database"
6+
"flare-indexer/indexer/config"
7+
"flare-indexer/indexer/context"
8+
"flare-indexer/indexer/pchain"
9+
"flare-indexer/indexer/shared"
10+
"flare-indexer/utils"
11+
"sort"
12+
"testing"
13+
"time"
14+
15+
"github.com/bradleyjkemp/cupaloy"
16+
"github.com/ethereum/go-ethereum/common"
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func uptimeVotingCronjobTestConfig(epochStart time.Time) *config.Config {
22+
cfg := &config.Config{
23+
Chain: globalConfig.ChainConfig{
24+
ChainAddressHRP: "localflare",
25+
ChainID: 31337,
26+
EthRPCURL: "http://127.0.0.1:8545",
27+
PrivateKey: "0xd49743deccbccc5dc7baa8e69e5be03298da8688a15dd202e20f15d5e0e9a9fb",
28+
},
29+
UptimeCronjob: config.UptimeConfig{
30+
CronjobConfig: config.CronjobConfig{
31+
Enabled: true,
32+
TimeoutSeconds: 30,
33+
},
34+
EpochConfig: config.EpochConfig{
35+
Start: utils.Timestamp{Time: epochStart},
36+
Period: 90 * time.Second,
37+
},
38+
VotingInterval: 60 * time.Second,
39+
EnableVoting: true,
40+
UptimeThreshold: 0.8,
41+
},
42+
VotingCronjob: config.VotingConfig{
43+
ContractAddress: common.HexToAddress("0x7c2C195CD6D34B8F845992d380aADB2730bB9C6F"),
44+
},
45+
PChainIndexer: config.IndexerConfig{
46+
Enabled: true,
47+
TimeoutMillis: 3000,
48+
BatchSize: 200,
49+
StartIndex: 0,
50+
},
51+
DB: globalConfig.DBConfig{
52+
Username: database.MysqlTestUser,
53+
Password: database.MysqlTestPassword,
54+
Host: database.MysqlTestHost,
55+
Port: database.MysqlTestPort,
56+
Database: "flare_indexer_indexer",
57+
LogQueries: false,
58+
},
59+
}
60+
return cfg
61+
62+
}
63+
64+
func createTestUptimeVotingCronjob(epochStart time.Time) (*uptimeVotingCronjob, *shared.ChainIndexerBase, error) {
65+
ctx, err := context.BuildTestContext(uptimeVotingCronjobTestConfig(epochStart))
66+
if err != nil {
67+
return nil, nil, err
68+
}
69+
cronjob, err := NewUptimeVotingCronjob(ctx)
70+
if err != nil {
71+
return nil, nil, err
72+
}
73+
74+
indexer := &shared.ChainIndexerBase{
75+
StateName: pchain.StateName,
76+
IndexerName: "P-chain Blocks Test",
77+
Client: testClient,
78+
DB: ctx.DB(),
79+
Config: ctx.Config().PChainIndexer,
80+
BatchIndexer: pchain.NewPChainBatchIndexer(ctx, testClient, testRPCClient),
81+
}
82+
return cronjob, indexer, nil
83+
}
84+
85+
// Requires a running hardhat node
86+
// from the flare-smart-contracts project, branch origin/staking-tests
87+
// with yarn staking_test
88+
func TestUptimeVoting(t *testing.T) {
89+
now := time.Unix(1675348249, 0)
90+
91+
// Epoch starts "now"
92+
votingCronjob, indexer, err := createTestUptimeVotingCronjob(now)
93+
require.NoError(t, err)
94+
95+
uptimeCronjob, err := createTestUptimeCronjob()
96+
require.NoError(t, err)
97+
98+
// Run indexer to allow uptime client test to fetch validator data
99+
err = indexer.IndexBatch()
100+
require.NoError(t, err)
101+
102+
testUptimeClient.SetNow(now)
103+
votingCronjob.time.SetNow(now)
104+
for i := 0; i < 10; i++ {
105+
if err := uptimeCronjob.Call(); err != nil {
106+
t.Fatal(err)
107+
}
108+
if err := votingCronjob.Call(); err != nil {
109+
t.Fatal(err)
110+
}
111+
testUptimeClient.Time.AdvanceNow(10 * time.Second)
112+
votingCronjob.time.AdvanceNow(10 * time.Second)
113+
}
114+
aggr, err := database.FetchAggregations(votingCronjob.db)
115+
require.NoError(t, err)
116+
assert.Equal(t, 4, len(aggr))
117+
118+
// Sort by nodeID and compare to snapshots
119+
sort.Slice(aggr, func(i, j int) bool {
120+
return aggr[i].NodeID < aggr[j].NodeID
121+
})
122+
aggrNodeIDs := utils.Map(aggr, func(a *database.UptimeAggregation) string {
123+
return a.NodeID
124+
})
125+
aggrValue := utils.Map(aggr, func(a *database.UptimeAggregation) int64 {
126+
return a.Value
127+
})
128+
cupaloy.SnapshotT(t, aggrNodeIDs, aggrValue)
129+
}

0 commit comments

Comments
 (0)