diff --git a/config.toml b/config.toml index fd6ef20..6eb37b6 100644 --- a/config.toml +++ b/config.toml @@ -27,7 +27,7 @@ chain_id = 162 eth_rpc_url = "http://localhost:9650/ext/bc/C/rpc" # api key may be needed for access to production nodes api_key = "" -# private_key_file is needed for voting and mirroring clients +# private_key_file is needed for mirroring client private_key_file = "path/to/private/key/file" [x_chain_indexer] @@ -52,12 +52,6 @@ period = "90s" uptime_threshold = 0.8 delete_old_uptimes_epoch_threshold = 5 -[voting_cronjob] -enabled = false -timeout = "10s" -# first epoch to be voted for (do not leave this empty since the first epoch is well back in time) -first = 1111 - [mirroring_cronjob] enabled = false timeout = "10s" diff --git a/indexer/config/config.go b/indexer/config/config.go index bb1983d..830bf0f 100644 --- a/indexer/config/config.go +++ b/indexer/config/config.go @@ -17,7 +17,6 @@ type Config struct { PChainIndexer IndexerConfig `toml:"p_chain_indexer"` UptimeCronjob UptimeConfig `toml:"uptime_cronjob"` Mirror MirrorConfig `toml:"mirroring_cronjob"` - VotingCronjob VotingConfig `toml:"voting_cronjob"` ContractAddresses ContractAddresses `toml:"contract_addresses"` } @@ -44,12 +43,6 @@ type MirrorConfig struct { config.EpochConfig } -type VotingConfig struct { - CronjobConfig - config.EpochConfig - GasLimit uint64 `toml:"gas_limit" envconfig:"VOTING_GAS_LIMIT"` -} - type UptimeConfig struct { CronjobConfig Period time.Duration `toml:"period" envconfig:"UPTIME_EPOCH_PERIOD"` diff --git a/indexer/cronjob/cronjob.go b/indexer/cronjob/cronjob.go index 43791e0..033f59f 100644 --- a/indexer/cronjob/cronjob.go +++ b/indexer/cronjob/cronjob.go @@ -1,7 +1,6 @@ package cronjob import ( - "flare-indexer/database" "flare-indexer/indexer/config" "flare-indexer/indexer/shared" "flare-indexer/logger" @@ -98,11 +97,6 @@ func (c *epochCronjob) UpdateCronjobStatus(status shared.HealthStatus) { } } -// Get processing range (closed interval) -func (c *epochCronjob) getEpochRange(start int64, now time.Time) *epochRange { - return c.getTrimmedEpochRange(start, c.epochs.GetEpochIndex(now)-1) -} - // Get trimmed processing range (closed interval) func (c *epochCronjob) getTrimmedEpochRange(start, end int64) *epochRange { start = utils.Max(start, c.epochs.First) @@ -118,11 +112,6 @@ func (c *epochCronjob) getTrimmedEpochRange(start, end int64) *epochRange { return &epochRange{start, end} } -func (c *epochCronjob) indexerBehind(idxState *database.State, epoch int64) bool { - epochEnd := c.epochs.GetEndTime(epoch) - return epochEnd.After(idxState.Updated.Add(-c.delay)) || idxState.NextDBIndex <= idxState.LastChainIndex -} - func (c *epochCronjob) updateLastEpochMetrics(epoch int64) { if c.metrics != nil { c.metrics.lastEpoch.Set(float64(epoch)) diff --git a/indexer/cronjob/migrations.go b/indexer/cronjob/migrations.go index c86b3d8..a4d0895 100644 --- a/indexer/cronjob/migrations.go +++ b/indexer/cronjob/migrations.go @@ -9,19 +9,9 @@ import ( ) func init() { - migrations.Container.Add("2023-08-25-00-00", "Create initial state for voting cronjob", createVotingCronjobState) migrations.Container.Add("2023-08-30-00-00", "Create initial state for mirror cronjob", createMirrorCronjobState) } -func createVotingCronjobState(db *gorm.DB) error { - return database.CreateState(db, &database.State{ - Name: votingStateName, - NextDBIndex: 0, - LastChainIndex: 0, - Updated: time.Now(), - }) -} - func createMirrorCronjobState(db *gorm.DB) error { return database.CreateState(db, &database.State{ Name: mirrorStateName, diff --git a/indexer/cronjob/voting.go b/indexer/cronjob/voting.go deleted file mode 100644 index 3bb40a2..0000000 --- a/indexer/cronjob/voting.go +++ /dev/null @@ -1,191 +0,0 @@ -package cronjob - -import ( - "flare-indexer/database" - indexerctx "flare-indexer/indexer/context" - "flare-indexer/indexer/pchain" - "flare-indexer/logger" - "flare-indexer/utils" - "flare-indexer/utils/staking" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/pkg/errors" -) - -const ( - votingStateName string = "voting_cronjob" -) - -var ( - zeroBytes [32]byte = [32]byte{} - zeroBytesHash common.Hash = crypto.Keccak256Hash(zeroBytes[:]) - - ErrEpochConfig = errors.New("epoch config mismatch") -) - -type votingCronjob struct { - epochCronjob - - db votingDB - contract votingContract - - // For testing to set "now" to some past date - time utils.ShiftedTime -} - -type votingDB interface { - FetchState(name string) (database.State, error) - FetchPChainVotingData(start, end time.Time) ([]database.PChainTxData, error) - UpdateState(state *database.State) error -} - -type votingContract interface { - ShouldVote(epoch *big.Int) (bool, error) - SubmitVote(epoch *big.Int, merkleRoot [32]byte) error - EpochConfig() (time.Time, time.Duration, error) -} - -func NewVotingCronjob(ctx indexerctx.IndexerContext) (*votingCronjob, error) { - cfg := ctx.Config() - if !cfg.VotingCronjob.Enabled { - return &votingCronjob{}, nil - } - - db := &votingDBGorm{g: ctx.DB()} - contract, err := newVotingContractCChain(cfg) - if err != nil { - return nil, err - } - - start, period, err := contract.EpochConfig() - if err != nil { - return nil, err - } - - epochs := staking.NewEpochInfo(&cfg.VotingCronjob.EpochConfig, start, period) - - vc := &votingCronjob{ - epochCronjob: newEpochCronjob(&cfg.VotingCronjob.CronjobConfig, epochs), - db: db, - contract: contract, - } - - err = vc.reset(ctx.Flags().ResetVotingCronjob) - if err != nil { - return nil, err - } - - vc.metrics = newEpochCronjobMetrics(votingStateName) - - return vc, nil -} - -func (c *votingCronjob) Name() string { - return votingStateName -} - -func (c *votingCronjob) OnStart() error { - return nil -} - -func (c *votingCronjob) RandomTimeoutDelta() time.Duration { - return 10 * time.Second -} - -func (c *votingCronjob) Call() error { - idxState, err := c.db.FetchState(pchain.StateName) - if err != nil { - return err - } - - state, err := c.db.FetchState(votingStateName) - if err != nil { - return err - } - - now := c.time.Now() - - // Last epoch that was submitted to the contract - epochRange := c.getEpochRange(int64(state.NextDBIndex), now) - - logger.Debug("Voting needed for epochs [%d, %d]", epochRange.start, epochRange.end) - c.updateLastEpochMetrics(epochRange.end) - - for e := epochRange.start; e <= epochRange.end; e++ { - start, end := c.epochs.GetTimeRange(e) - - if c.indexerBehind(&idxState, e) { - logger.Debug("indexer is behind, skipping voting for epoch %d", e) - return nil - } - - votingData, err := c.db.FetchPChainVotingData(start, end) - if err != nil { - return err - } - err = c.submitVotes(e, votingData) - if err != nil { - return err - } - state.NextDBIndex = uint64(e + 1) - if err := c.db.UpdateState(&state); err != nil { - return err - } - c.updateLastProcessedEpochMetrics(e) - } - return nil -} - -// Return true if the vote was submitted, and false if shouldVote returned false -func (c *votingCronjob) submitVotes(e int64, votingData []database.PChainTxData) error { - votingData = staking.DedupeTxs(votingData) - - shouldVote, err := c.contract.ShouldVote(big.NewInt(e)) - if err != nil { - return err - } - if !shouldVote { - logger.Debug("Voting not needed for epoch %d", e) - return nil - } - - var merkleRoot common.Hash - if len(votingData) == 0 { - merkleRoot = zeroBytesHash - } else { - merkleRoot, err = staking.GetMerkleRoot(votingData) - if err != nil { - return err - } - } - - // Submit vote and wait for the transaction to be mined - err = c.contract.SubmitVote(big.NewInt(e), [32]byte(merkleRoot)) - if err != nil { - return err - } - logger.Info("Submitted vote for epoch %d", e) - return nil -} - -func (c *votingCronjob) reset(firstEpoch int64) error { - if firstEpoch <= 0 { - return nil - } - - logger.Info("Resetting voting cronjob state to epoch %d", firstEpoch) - state, err := c.db.FetchState(votingStateName) - if err != nil { - return err - } - state.NextDBIndex = uint64(firstEpoch) - err = c.db.UpdateState(&state) - if err != nil { - return err - } - c.epochs.First = firstEpoch - return nil -} diff --git a/indexer/cronjob/voting_integration_test.go b/indexer/cronjob/voting_integration_test.go deleted file mode 100644 index 9b2a7cb..0000000 --- a/indexer/cronjob/voting_integration_test.go +++ /dev/null @@ -1,180 +0,0 @@ -//go:build integration -// +build integration - -package cronjob - -import ( - sysContext "context" - globalConfig "flare-indexer/config" - "flare-indexer/database" - "flare-indexer/indexer/config" - "flare-indexer/indexer/context" - "flare-indexer/indexer/pchain" - "flare-indexer/indexer/shared" - "flare-indexer/utils" - "flare-indexer/utils/contracts/voting" - "math/big" - "testing" - "time" - - "github.com/bradleyjkemp/cupaloy" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" -) - -func votingCronjobTestConfig(epochStart time.Time, dbName string, privateKey string) *config.Config { - cfg := &config.Config{ - Chain: globalConfig.ChainConfig{ - ChainAddressHRP: "localflare", - ChainID: 31337, - EthRPCURL: "http://127.0.0.1:8545", - PrivateKey: privateKey, - }, - VotingCronjob: config.VotingConfig{ - CronjobConfig: config.CronjobConfig{ - Enabled: true, - Timeout: 30 * time.Second, - }, - ContractAddress: common.HexToAddress("0x7c2C195CD6D34B8F845992d380aADB2730bB9C6F"), - }, - Mirror: config.MirrorConfig{ - CronjobConfig: config.CronjobConfig{ - Enabled: true, - Timeout: 30 * time.Second, - }, - MirroringContract: common.HexToAddress("0x8858eeB3DfffA017D4BCE9801D340D36Cf895CCf"), - }, - Epochs: config.EpochConfig{ - Start: utils.Timestamp{Time: epochStart}, - Period: 90 * time.Second, - }, - PChainIndexer: config.IndexerConfig{ - Enabled: true, - Timeout: 3000 * time.Millisecond, - BatchSize: 200, - StartIndex: 0, - }, - DB: globalConfig.DBConfig{ - Username: database.MysqlTestUser, - Password: database.MysqlTestPassword, - Host: database.MysqlTestHost, - Port: database.MysqlTestPort, - Database: dbName, - LogQueries: false, - }, - Logger: globalConfig.LoggerConfig{ - Level: "debug", - }, - } - return cfg -} - -// Transform p-chain txs before persisting: -// Extend end time of a validator tx past test start time to prevent mirror contract to fail -func transformPChainTx(tx *database.PChainTx) *database.PChainTx { - if tx.Type == database.PChainAddValidatorTx || tx.Type == database.PChainAddDelegatorTx { - minEndTime := time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC) - tx.EndTime = &minEndTime - } - return tx -} - -func createTestVotingClients(epochStart time.Time) (*votingCronjob, *votingCronjob, *mirrorCronJob, *shared.ChainIndexerBase, *shared.ChainIndexerBase, error) { - ctx1, err := context.BuildTestContext(votingCronjobTestConfig(epochStart, "flare_indexer_indexer", privateKey1)) - if err != nil { - return nil, nil, nil, nil, nil, err - } - cronjob1, err := NewVotingCronjob(ctx1) - if err != nil { - return nil, nil, nil, nil, nil, err - } - ctx2, err := context.BuildTestContext(votingCronjobTestConfig(epochStart, "flare_indexer_indexer_2", privateKey2)) - if err != nil { - return nil, nil, nil, nil, nil, err - } - cronjob2, err := NewVotingCronjob(ctx2) - if err != nil { - return nil, nil, nil, nil, nil, err - } - mirror, err := NewMirrorCronjob(ctx1) - if err != nil { - return nil, nil, nil, nil, nil, err - } - - indexer1 := &shared.ChainIndexerBase{ - StateName: pchain.StateName, - IndexerName: "P-chain Blocks Test", - Client: testClient, - DB: ctx1.DB(), - Config: ctx1.Config().PChainIndexer, - BatchIndexer: pchain.NewPChainBatchIndexer( - ctx1, testClient, testRPCClient, - pchain.NewPChainDataTransformer(transformPChainTx), - ), - } - indexer2 := &shared.ChainIndexerBase{ - StateName: pchain.StateName, - IndexerName: "P-chain Blocks Test", - Client: testClient, - DB: ctx2.DB(), - Config: ctx2.Config().PChainIndexer, - BatchIndexer: pchain.NewPChainBatchIndexer( - ctx1, testClient, testRPCClient, - pchain.NewPChainDataTransformer(transformPChainTx), - ), - } - return cronjob1, cronjob2, mirror.(*mirrorCronJob), indexer1, indexer2, nil -} - -func getMerkleRootFromContract(votingContract *voting.Voting, epoch int64) ([32]byte, error) { - ctx := sysContext.Background() - opts := &bind.CallOpts{Context: ctx} - merkleRoot, err := votingContract.GetMerkleRoot(opts, big.NewInt(epoch)) - if err != nil { - return [32]byte{}, err - } - return merkleRoot, nil -} - -func TestVoting(t *testing.T) { - now := time.Unix(1675349340, 0) // 2023-02-02 14:49:00 UTC - vCronjob1, vCronjob2, mCronjob, indexer1, indexer2, err := createTestVotingClients(now) - require.NoError(t, err) - - // Run indexer to allow voting client test to fetch validator data - // We need two indexers, each one for a different voting client, - // since the progress is stored in the DB - t.Run("Run indexer 1", func(t *testing.T) { - err := indexer1.IndexBatch() - require.NoError(t, err) - }) - t.Run("Run indexer 2", func(t *testing.T) { - err := indexer2.IndexBatch() - require.NoError(t, err) - }) - - t.Run("Run voting clients 1 and 2", func(t *testing.T) { - vCronjob1.time.SetNow(now) - vCronjob2.time.SetNow(now) - for i := 0; i < 10; i++ { - err := vCronjob1.Call() - require.NoError(t, err) - err = vCronjob2.Call() - require.NoError(t, err) - vCronjob1.time.AdvanceNow(30 * time.Second) - vCronjob2.time.AdvanceNow(30 * time.Second) - } - }) - t.Run("Verify merkle root", func(t *testing.T) { - root, err := getMerkleRootFromContract(vCronjob1.votingContract, 0) - require.NoError(t, err) - cupaloy.SnapshotT(t, root) - }) - t.Run("Run mirroring client", func(t *testing.T) { - mCronjob.time.SetNow(now) - mCronjob.time.AdvanceNow(10 * 30 * time.Second) - err := mCronjob.Call() - require.NoError(t, err) - }) -} diff --git a/indexer/cronjob/voting_stubs.go b/indexer/cronjob/voting_stubs.go deleted file mode 100644 index 03f4cf3..0000000 --- a/indexer/cronjob/voting_stubs.go +++ /dev/null @@ -1,96 +0,0 @@ -package cronjob - -import ( - "flare-indexer/database" - "flare-indexer/indexer/config" - "flare-indexer/logger" - "flare-indexer/utils/chain" - "flare-indexer/utils/contracts/voting" - "flare-indexer/utils/staking" - "math/big" - "strings" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "gorm.io/gorm" -) - -type votingDBGorm struct { - g *gorm.DB -} - -func (db *votingDBGorm) FetchState(name string) (database.State, error) { - return database.FetchState(db.g, name) -} - -func (db *votingDBGorm) FetchPChainVotingData(start, end time.Time) ([]database.PChainTxData, error) { - return database.FetchPChainVotingData(db.g, start, end) -} - -func (db *votingDBGorm) UpdateState(state *database.State) error { - return database.UpdateState(db.g, state) -} - -type votingContractCChain struct { - callOpts *bind.CallOpts - txOpts *bind.TransactOpts - voting *voting.Voting - txVerifier *chain.TxVerifier -} - -func newVotingContractCChain(cfg *config.Config) (votingContract, error) { - eth, err := cfg.Chain.DialETH() - if err != nil { - return nil, err - } - - votingContract, err := voting.NewVoting(cfg.ContractAddresses.Voting, eth) - if err != nil { - return nil, err - } - - privateKey, err := cfg.Chain.GetPrivateKey() - if err != nil { - return nil, err - } - - txOpts, err := TransactOptsFromPrivateKey(privateKey, cfg.Chain.ChainID) - if err != nil { - return nil, err - } - txOpts.GasLimit = cfg.VotingCronjob.GasLimit - - callOpts := &bind.CallOpts{From: txOpts.From} - - return &votingContractCChain{ - callOpts: callOpts, - txOpts: txOpts, - voting: votingContract, - txVerifier: chain.NewTxVerifier(eth), - }, nil -} - -func (c *votingContractCChain) ShouldVote(epoch *big.Int) (bool, error) { - return c.voting.ShouldVote(c.callOpts, epoch, c.callOpts.From) -} - -func (c *votingContractCChain) SubmitVote(epoch *big.Int, merkleRoot [32]byte) error { - tx, err := c.voting.SubmitVote(c.txOpts, epoch, merkleRoot) - if err != nil { - return err - } - err = c.txVerifier.WaitUntilMined(c.callOpts.From, tx, chain.DefaultTxTimeout) - if err != nil { - if strings.Contains(err.Error(), "epoch already finalized") { - logger.Info("Epoch %s already finalized", epoch.String()) - return nil - } - return err - } - logger.Debug("Mined voting tx %s", tx.Hash().Hex()) - return nil -} - -func (c *votingContractCChain) EpochConfig() (start time.Time, period time.Duration, err error) { - return staking.GetEpochConfig(c.voting) -} diff --git a/indexer/cronjob/voting_test.go b/indexer/cronjob/voting_test.go deleted file mode 100644 index 7a6214a..0000000 --- a/indexer/cronjob/voting_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package cronjob - -import ( - "flare-indexer/database" - "flare-indexer/indexer/config" - "flare-indexer/indexer/pchain" - "flare-indexer/utils/staking" - "math/big" - "testing" - "time" - - "github.com/bradleyjkemp/cupaloy" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" -) - -type votingDBTest struct { - states map[string]database.State - votingData map[timeRange][]database.PChainTxData -} - -type timeRange struct { - start time.Time - end time.Time -} - -func (db *votingDBTest) FetchState(name string) (database.State, error) { - state, ok := db.states[name] - if ok { - return state, nil - } - - return database.State{Name: name}, nil -} - -func (db *votingDBTest) FetchPChainVotingData(start, end time.Time) ([]database.PChainTxData, error) { - return db.votingData[timeRange{start, end}], nil -} - -func (db *votingDBTest) UpdateState(state *database.State) error { - db.states[state.Name] = *state - return nil -} - -type votingContractTest struct { - shouldVote map[int64]bool - submittedVotes map[int64][32]byte -} - -func (c *votingContractTest) ShouldVote(epoch *big.Int) (bool, error) { - return c.shouldVote[epoch.Int64()], nil -} - -func (c *votingContractTest) SubmitVote(epoch *big.Int, merkleRoot [32]byte) error { - epochInt := epoch.Int64() - - if _, ok := c.submittedVotes[epochInt]; ok { - return errors.New("already submitted vote") - } - - c.submittedVotes[epochInt] = merkleRoot - c.shouldVote[epochInt] = false - return nil -} - -func (c *votingContractTest) EpochConfig() (time.Time, time.Duration, error) { - return time.Now(), 180 * time.Second, nil -} - -func TestNoVotes(t *testing.T) { - db := &votingDBTest{} - contract := &votingContractTest{} - cronjob := &votingCronjob{ - db: db, - contract: contract, - epochCronjob: initEpochCronjob(), - } - - err := cronjob.Call() - require.NoError(t, err) - require.Empty(t, contract.submittedVotes) -} - -func TestVotes(t *testing.T) { - epochs := initEpochCronjob() - - db := votingDBTest{ - states: map[string]database.State{ - pchain.StateName: { - Updated: time.Now(), - NextDBIndex: 3, - LastChainIndex: 2, - }, - }, - votingData: map[timeRange][]database.PChainTxData{ - timeRangeForEpoch(epochs, 1): {newTxData(0)}, - timeRangeForEpoch(epochs, 2): {newTxData(1), newTxData(2)}, - }, - } - - contract := votingContractTest{ - shouldVote: map[int64]bool{ - 1: true, - 2: true, - }, - submittedVotes: make(map[int64][32]byte), - } - - cronjob := votingCronjob{ - db: &db, - contract: &contract, - epochCronjob: epochs, - } - - err := cronjob.Call() - require.NoError(t, err) - require.NotEmpty(t, contract.submittedVotes) - - cupaloy.SnapshotT(t, contract.submittedVotes) - - updatedState := db.states[votingStateName] - require.Equal(t, updatedState.NextDBIndex, uint64(5)) -} - -func timeRangeForEpoch(cj epochCronjob, epoch int64) timeRange { - start, end := cj.epochs.GetTimeRange(epoch) - - return timeRange{start, end} -} - -var txIDs = []string{ - "XnfV79XVMyuXbTw8iNreQ9FrUgy9csYBJp1xRscay3oDzhyq8", - "nsPmyQbm4oo77jyykxbjf7s4Zp4urNptkyAouxVWZ2EB2kw1z", - "2p32tpqNrfzP3SStbP9bQGHZtJkCxjV3iHNssVnkcpUWxHMSuj", -} - -func newTxData(id int) database.PChainTxData { - startTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - endTime := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC) - - return database.PChainTxData{ - PChainTx: database.PChainTx{ - ChainID: "costwo", - NodeID: "NodeID-CZYx3on11wwYXFoHwZtAQZT5unZ9JHMf6", - StartTime: &startTime, - EndTime: &endTime, - TxID: &txIDs[id], - Type: database.PChainAddDelegatorTx, - }, - InputAddress: "costwo18atl0e95w5ym6t8u5yrjpz35vqqzxfzrrsnq8u", - } -} - -func initEpochCronjob() epochCronjob { - cronjobCfg := config.CronjobConfig{ - Enabled: true, - Timeout: 180, - BatchSize: 5, - } - - epochInfo := staking.EpochInfo{ - Period: 180 * time.Second, - Start: time.Now().Add(-time.Hour), - } - - return newEpochCronjob(&cronjobCfg, epochInfo) -} diff --git a/indexer/runner/runner.go b/indexer/runner/runner.go index 414cdbf..ee583f5 100644 --- a/indexer/runner/runner.go +++ b/indexer/runner/runner.go @@ -12,10 +12,6 @@ func Start(ctx context.IndexerContext) { xIndexer := xchain.CreateXChainTxIndexer(ctx) pIndexer := pchain.CreatePChainBlockIndexer(ctx) - votingCronjob, err := cronjob.NewVotingCronjob(ctx) - if err != nil { - log.Fatal(err) - } mirrorCronjob, err := cronjob.NewMirrorCronjob(ctx) if err != nil { log.Fatal(err) @@ -30,7 +26,6 @@ func Start(ctx context.IndexerContext) { go pIndexer.Run() go cronjob.RunCronjob(uptimeCronjob) - go cronjob.RunCronjob(votingCronjob) go cronjob.RunCronjob(mirrorCronjob) go cronjob.RunCronjob(uptimeVotingCronjob) } diff --git a/services/api/pchain.go b/services/api/pchain.go index 7f8e9d3..7ee8e6c 100644 --- a/services/api/pchain.go +++ b/services/api/pchain.go @@ -3,6 +3,8 @@ package api import ( "flare-indexer/database" "time" + + "github.com/ethereum/go-ethereum/common" ) type ApiPChainTx struct { @@ -104,3 +106,13 @@ func NewApiPChainTxList(txs []database.PChainTxData) []ApiPChainTxListItem { return result } + +type ApiMerkleRoot struct { + Root string `json:"root"` +} + +func NewApiMerkleRoot(root common.Hash) *ApiMerkleRoot { + return &ApiMerkleRoot{ + Root: root.Hex(), + } +} diff --git a/services/routes/transactions.go b/services/routes/transactions.go index daa8eb4..d26e899 100644 --- a/services/routes/transactions.go +++ b/services/routes/transactions.go @@ -24,7 +24,7 @@ func newTransactionRouteHandlers(ctx context.ServicesContext, epochs staking.Epo } } -func (rh *transactionRouteHandlers) getTransaction() utils.RouteHandler { +func (rh *transactionRouteHandlers) getTransactionHandler() utils.RouteHandler { handler := func(params map[string]string) (*api.ApiPChainTx, *utils.ErrorHandler) { txID := params["tx_id"] var resp *api.ApiPChainTx = nil @@ -45,30 +45,64 @@ func (rh *transactionRouteHandlers) getTransaction() utils.RouteHandler { &api.ApiPChainTx{}) } -func (rh *transactionRouteHandlers) listTransactions() utils.RouteHandler { +func (rh *transactionRouteHandlers) listTransactionsHandler() utils.RouteHandler { handler := func(params map[string]string) ([]api.ApiPChainTxListItem, *utils.ErrorHandler) { - epoch, err := strconv.ParseInt(params["epoch"], 10, 64) - if err != nil { - return nil, utils.HttpErrorHandler(http.StatusBadRequest, "Invalid epoch") + txs, errHandler := rh.listTransactionsForEpoch(params) + if errHandler != nil { + return nil, errHandler } - startTimestamp, endTimestamp := rh.epochs.GetTimeRange(epoch) - txs, err := database.GetPChainTxsForEpoch(&database.GetPChainTxsForEpochInput{ - DB: rh.db, - StartTimestamp: startTimestamp, - EndTimestamp: endTimestamp, - }) + return api.NewApiPChainTxList(txs), nil + } + + return utils.NewParamRouteHandler(handler, http.MethodGet, + map[string]string{"epoch:[0-9]+": "Epoch"}, + []api.ApiPChainTxListItem{}, + ) +} + +func (rh *transactionRouteHandlers) merkleRootHandler() utils.RouteHandler { + handler := func(params map[string]string) (*api.ApiMerkleRoot, *utils.ErrorHandler) { + txs, errHandler := rh.listTransactionsForEpoch(params) + if errHandler != nil { + return nil, errHandler + } + + merkleRoot, err := staking.GetMerkleRoot(txs) if err != nil { return nil, utils.InternalServerErrorHandler(err) } - return api.NewApiPChainTxList(txs), nil + return api.NewApiMerkleRoot(merkleRoot), nil } return utils.NewParamRouteHandler(handler, http.MethodGet, map[string]string{"epoch:[0-9]+": "Epoch"}, - []api.ApiPChainTxListItem{}, + new(api.ApiMerkleRoot), ) + +} + +func (rh *transactionRouteHandlers) listTransactionsForEpoch( + params map[string]string, +) ([]database.PChainTxData, *utils.ErrorHandler) { + epoch, err := strconv.ParseInt(params["epoch"], 10, 64) + if err != nil { + return nil, utils.HttpErrorHandler(http.StatusBadRequest, "Invalid epoch") + } + + startTimestamp, endTimestamp := rh.epochs.GetTimeRange(epoch) + + txs, err := database.GetPChainTxsForEpoch(&database.GetPChainTxsForEpochInput{ + DB: rh.db, + StartTimestamp: startTimestamp, + EndTimestamp: endTimestamp, + }) + if err != nil { + return nil, utils.InternalServerErrorHandler(err) + } + + return txs, nil } func AddTransactionRoutes( @@ -76,6 +110,7 @@ func AddTransactionRoutes( ) { vr := newTransactionRouteHandlers(ctx, epochs) subrouter := router.WithPrefix("/transactions", "Transactions") - subrouter.AddRoute("/get/{tx_id:[0-9a-zA-Z]+}", vr.getTransaction()) - subrouter.AddRoute("/list/{epoch:[0-9]+}", vr.listTransactions()) + subrouter.AddRoute("/get/{tx_id:[0-9a-zA-Z]+}", vr.getTransactionHandler()) + subrouter.AddRoute("/list/{epoch:[0-9]+}", vr.listTransactionsHandler()) + subrouter.AddRoute("/merkle-root/{epoch:[0-9]+}", vr.merkleRootHandler()) } diff --git a/utils/merkle/merkle.go b/utils/merkle/merkle.go index 0b1ebbb..4e3cd12 100644 --- a/utils/merkle/merkle.go +++ b/utils/merkle/merkle.go @@ -39,6 +39,10 @@ func NewFromHex(hexValues []string) Tree { // Given an array of leaf hashes, builds the Merkle tree. func Build(hashes []common.Hash, initialHash bool) Tree { + if len(hashes) == 0 { + return New(nil) + } + if initialHash { hashes = mapSingleHash(hashes) } diff --git a/utils/staking/utils.go b/utils/staking/utils.go index 802c2fe..f5a4fd1 100644 --- a/utils/staking/utils.go +++ b/utils/staking/utils.go @@ -17,6 +17,8 @@ import ( var ( merkleTreeItemABIObjectArguments abi.Arguments + zeroBytes [32]byte = [32]byte{} + zeroBytesHash common.Hash = crypto.Keccak256Hash(zeroBytes[:]) ) func init() { @@ -185,6 +187,10 @@ func BuildTree(txs []database.PChainTxData) (merkle.Tree, error) { } func GetMerkleRoot(votingData []database.PChainTxData) (common.Hash, error) { + if len(votingData) == 0 { + return zeroBytesHash, nil + } + tree, err := BuildTree(votingData) if err != nil { return [32]byte{}, err