From c82028aa4f3c3228341f9dbdd7ecfb53387c8102 Mon Sep 17 00:00:00 2001 From: pk910 Date: Sun, 10 Sep 2023 05:07:00 +0200 Subject: [PATCH] track full finalization checkpoints (finalized & justified) --- handlers/epochs.go | 3 ++- handlers/index.go | 8 ++++--- indexer/cache.go | 23 ++++++++++++------- indexer/client.go | 38 +++++++++++++++++++------------ indexer/epoch_stats.go | 16 +++++++------ indexer/indexer.go | 4 ++-- indexer/synchronizer.go | 2 +- rpc/beaconapi.go | 15 ++++-------- rpctypes/beaconapi.go | 9 ++++++++ services/beaconservice.go | 17 +++++++------- templates/epochs/epochs.html | 2 ++ templates/index/recentEpochs.html | 5 +++- types/models/epochs.go | 1 + types/models/indexPage.go | 2 ++ 14 files changed, 90 insertions(+), 55 deletions(-) diff --git a/handlers/epochs.go b/handlers/epochs.go index dc155573..e14d6874 100644 --- a/handlers/epochs.go +++ b/handlers/epochs.go @@ -90,7 +90,7 @@ func buildEpochsPageData(firstEpoch uint64, pageSize uint64) (*models.EpochsPage } pageData.LastPageEpoch = pageSize - 1 - finalizedEpoch, _ := services.GlobalBeaconService.GetFinalizedEpoch() + finalizedEpoch, _, justifiedEpoch, _ := services.GlobalBeaconService.GetIndexer().GetFinalizationCheckpoints() epochLimit := pageSize // load epochs @@ -111,6 +111,7 @@ func buildEpochsPageData(firstEpoch uint64, pageSize uint64) (*models.EpochsPage Epoch: epoch, Ts: utils.EpochToTime(epoch), Finalized: finalized, + Justified: justifiedEpoch >= epochIdx, } if dbIdx < dbCnt && dbEpochs[dbIdx] != nil && dbEpochs[dbIdx].Epoch == epoch { dbEpoch := dbEpochs[dbIdx] diff --git a/handlers/index.go b/handlers/index.go index be95c34f..69f6d0c9 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -79,7 +79,7 @@ func buildIndexPageData() (*models.IndexPageData, time.Duration) { currentSlot := utils.TimeToSlot(uint64(now.Unix())) currentSlotIndex := (currentSlot % utils.Config.Chain.Config.SlotsPerEpoch) + 1 - finalizedEpoch, _ := services.GlobalBeaconService.GetFinalizedEpoch() + finalizedEpoch, _, justifiedEpoch, _ := services.GlobalBeaconService.GetIndexer().GetFinalizationCheckpoints() syncState := dbtypes.IndexerSyncState{} db.GetExplorerState("indexer.syncstate", &syncState) @@ -97,6 +97,7 @@ func buildIndexPageData() (*models.IndexPageData, time.Duration) { SlotsPerEpoch: utils.Config.Chain.Config.SlotsPerEpoch, CurrentEpoch: uint64(currentEpoch), CurrentFinalizedEpoch: finalizedEpoch, + CurrentJustifiedEpoch: justifiedEpoch, CurrentSlot: currentSlot, CurrentScheduledCount: utils.Config.Chain.Config.SlotsPerEpoch - currentSlotIndex, CurrentEpochProgress: float64(100) * float64(currentSlotIndex) / float64(utils.Config.Chain.Config.SlotsPerEpoch), @@ -175,7 +176,7 @@ func buildIndexPageData() (*models.IndexPageData, time.Duration) { } // load recent epochs - buildIndexPageRecentEpochsData(pageData, uint64(currentEpoch), finalizedEpoch, recentEpochCount) + buildIndexPageRecentEpochsData(pageData, uint64(currentEpoch), finalizedEpoch, justifiedEpoch, recentEpochCount) // load recent blocks buildIndexPageRecentBlocksData(pageData, currentSlot, recentBlockCount) @@ -186,7 +187,7 @@ func buildIndexPageData() (*models.IndexPageData, time.Duration) { return pageData, 12 * time.Second } -func buildIndexPageRecentEpochsData(pageData *models.IndexPageData, currentEpoch uint64, finalizedEpoch int64, recentEpochCount int) { +func buildIndexPageRecentEpochsData(pageData *models.IndexPageData, currentEpoch uint64, finalizedEpoch int64, justifiedEpoch int64, recentEpochCount int) { pageData.RecentEpochs = make([]*models.IndexPageDataEpochs, 0) epochsData := services.GlobalBeaconService.GetDbEpochs(currentEpoch, uint32(recentEpochCount)) for i := 0; i < len(epochsData); i++ { @@ -202,6 +203,7 @@ func buildIndexPageRecentEpochsData(pageData *models.IndexPageData, currentEpoch Epoch: epochData.Epoch, Ts: utils.EpochToTime(epochData.Epoch), Finalized: finalizedEpoch >= int64(epochData.Epoch), + Justified: justifiedEpoch >= int64(epochData.Epoch), EligibleEther: epochData.Eligible, TargetVoted: epochData.VotedTarget, VoteParticipation: voteParticipation, diff --git a/indexer/cache.go b/indexer/cache.go index 434903d1..2bf1fa9d 100644 --- a/indexer/cache.go +++ b/indexer/cache.go @@ -19,6 +19,8 @@ type indexerCache struct { lowestSlot int64 finalizedEpoch int64 finalizedRoot []byte + justifiedEpoch int64 + justifiedRoot []byte processedEpoch int64 persistEpoch int64 cleanupEpoch int64 @@ -68,12 +70,17 @@ func (cache *indexerCache) startSynchronizer(startEpoch uint64) { } } -func (cache *indexerCache) setFinalizedHead(epoch int64, root []byte) { +func (cache *indexerCache) setFinalizedHead(finalizedEpoch int64, finalizedRoot []byte, justifiedEpoch int64, justifiedRoot []byte) { cache.cacheMutex.Lock() defer cache.cacheMutex.Unlock() - if epoch > cache.finalizedEpoch { - cache.finalizedEpoch = epoch - cache.finalizedRoot = root + + if justifiedEpoch > cache.justifiedEpoch { + cache.justifiedEpoch = justifiedEpoch + cache.justifiedRoot = justifiedRoot + } + if finalizedEpoch > cache.finalizedEpoch { + cache.finalizedEpoch = finalizedEpoch + cache.finalizedRoot = finalizedRoot // trigger processing cache.triggerChan <- true @@ -88,10 +95,10 @@ func (cache *indexerCache) setGenesis(genesis *rpctypes.StandardV1GenesisRespons } } -func (cache *indexerCache) getFinalizedHead() (int64, []byte) { +func (cache *indexerCache) getFinalizationCheckpoints() (int64, []byte, int64, []byte) { cache.cacheMutex.RLock() defer cache.cacheMutex.RUnlock() - return cache.finalizedEpoch, cache.finalizedRoot + return cache.finalizedEpoch, cache.finalizedRoot, cache.justifiedEpoch, cache.justifiedRoot } func (cache *indexerCache) setLastValidators(epoch uint64, validators *rpctypes.StandardV1StateValidatorsResponse) { @@ -158,7 +165,7 @@ func (cache *indexerCache) isCanonicalBlock(blockRoot []byte, head []byte) bool func (cache *indexerCache) getCanonicalDistance(blockRoot []byte, head []byte) (bool, uint64) { if head == nil { - head = cache.finalizedRoot + head = cache.justifiedRoot } block := cache.getCachedBlock(blockRoot) var blockSlot uint64 @@ -197,7 +204,7 @@ func (cache *indexerCache) getCanonicalDistance(blockRoot []byte, head []byte) ( func (cache *indexerCache) getLastCanonicalBlock(epoch uint64, head []byte) *CacheBlock { if head == nil { - head = cache.finalizedRoot + head = cache.justifiedRoot } canonicalBlock := cache.getCachedBlock(head) for canonicalBlock != nil && utils.EpochOfSlot(canonicalBlock.Slot) > epoch { diff --git a/indexer/client.go b/indexer/client.go index 505c30e0..0b14a9a2 100644 --- a/indexer/client.go +++ b/indexer/client.go @@ -31,6 +31,8 @@ type IndexerClient struct { lastEpochStats int64 lastFinalizedEpoch int64 lastFinalizedRoot []byte + lastJustifiedEpoch int64 + lastJustifiedRoot []byte } func newIndexerClient(clientIdx uint8, clientName string, rpcClient *rpc.BeaconClient, indexerCache *indexerCache, archive bool, priority int, skipValidators bool) *IndexerClient { @@ -45,6 +47,7 @@ func newIndexerClient(clientIdx uint8, clientName string, rpcClient *rpc.BeaconC lastHeadSlot: -1, lastEpochStats: -1, lastFinalizedEpoch: -1, + lastJustifiedEpoch: -1, } go client.runIndexerClientLoop() return &client @@ -189,18 +192,10 @@ func (client *IndexerClient) runIndexerClient() error { } // get finalized header - finalizedHeader, err := client.rpcClient.GetFinalizedBlockHead() + finalizedSlot, err := client.refreshFinalityCheckpoints() if err != nil { logger.WithField("client", client.clientName).Warnf("could not get finalized header: %v", err) } - var finalizedSlot uint64 - if finalizedHeader != nil { - client.cacheMutex.Lock() - finalizedSlot = uint64(finalizedHeader.Data.Header.Message.Slot) - client.lastFinalizedEpoch = int64(utils.EpochOfSlot(uint64(finalizedHeader.Data.Header.Message.Slot)) - 1) - client.lastFinalizedRoot = finalizedHeader.Data.Root - client.cacheMutex.Unlock() - } logger.WithField("client", client.clientName).Debugf("endpoint %v ready: %v ", client.clientName, client.versionStr) client.retryCounter = 0 @@ -216,9 +211,7 @@ func (client *IndexerClient) runIndexerClient() error { } // set finalized head and trigger epoch processing / synchronization - if finalizedHeader != nil { - client.indexerCache.setFinalizedHead(client.lastFinalizedEpoch, client.lastFinalizedRoot) - } + client.indexerCache.setFinalizedHead(client.lastFinalizedEpoch, client.lastFinalizedRoot, client.lastJustifiedEpoch, client.lastJustifiedRoot) // process events client.lastStreamEvent = time.Now() @@ -267,6 +260,22 @@ func (client *IndexerClient) runIndexerClient() error { } } +func (client *IndexerClient) refreshFinalityCheckpoints() (uint64, error) { + finalizedCheckpoints, err := client.rpcClient.GetFinalityCheckpoints() + if err != nil { + return 0, err + } + var finalizedSlot uint64 + client.cacheMutex.Lock() + finalizedSlot = uint64(finalizedCheckpoints.Data.Finalized.Epoch) * utils.Config.Chain.Config.SlotsPerEpoch + client.lastFinalizedEpoch = int64(finalizedCheckpoints.Data.Finalized.Epoch) - 1 + client.lastFinalizedRoot = finalizedCheckpoints.Data.Finalized.Root + client.lastJustifiedEpoch = int64(finalizedCheckpoints.Data.CurrentJustified.Epoch) - 1 + client.lastJustifiedRoot = finalizedCheckpoints.Data.CurrentJustified.Root + client.cacheMutex.Unlock() + return finalizedSlot, nil +} + func (client *IndexerClient) prefillCache(finalizedSlot uint64, latestHeader *rpctypes.StandardV1BeaconHeaderResponse) error { currentBlock, isNewBlock := client.indexerCache.createOrGetCachedBlock(latestHeader.Data.Root, uint64(latestHeader.Data.Header.Message.Slot)) if isNewBlock { @@ -490,7 +499,8 @@ func (client *IndexerClient) processBlockEvent(evt *rpctypes.StandardV1StreamedB } func (client *IndexerClient) processFinalizedEvent(evt *rpctypes.StandardV1StreamedFinalizedCheckpointEvent) error { - logger.WithField("client", client.clientName).Debugf("received finalization_checkpoint event: epoch %v [%s]", evt.Epoch, evt.Block.String()) - client.indexerCache.setFinalizedHead(int64(evt.Epoch)-1, evt.Block) + client.refreshFinalityCheckpoints() + logger.WithField("client", client.clientName).Debugf("received finalization_checkpoint event: finalized %v [0x%x], justified %v [0x%x]", client.lastFinalizedEpoch, client.lastFinalizedRoot, client.lastJustifiedEpoch, client.lastJustifiedRoot) + client.indexerCache.setFinalizedHead(client.lastFinalizedEpoch, client.lastFinalizedRoot, client.lastJustifiedEpoch, client.lastJustifiedRoot) return nil } diff --git a/indexer/epoch_stats.go b/indexer/epoch_stats.go index abaa9d9d..3df93a35 100644 --- a/indexer/epoch_stats.go +++ b/indexer/epoch_stats.go @@ -314,14 +314,16 @@ func (epochStats *EpochStats) ensureEpochStatsLazy(client *IndexerClient, propos if err != nil { logger.WithField("client", client.clientName).Warnf("error retrieving sync_committees for epoch %v (state: %v): %v", epochStats.Epoch, syncCommitteeState, err) } - epochStats.syncAssignments = make([]uint64, len(parsedSyncCommittees.Data.Validators)) - for i, valIndexStr := range parsedSyncCommittees.Data.Validators { - valIndexU64, err := strconv.ParseUint(valIndexStr, 10, 64) - if err != nil { - logger.WithField("client", client.clientName).Warnf("in sync_committee for epoch %d validator %d has bad validator index: %q", epochStats.Epoch, i, valIndexStr) - continue + if parsedSyncCommittees != nil { + epochStats.syncAssignments = make([]uint64, len(parsedSyncCommittees.Data.Validators)) + for i, valIndexStr := range parsedSyncCommittees.Data.Validators { + valIndexU64, err := strconv.ParseUint(valIndexStr, 10, 64) + if err != nil { + logger.WithField("client", client.clientName).Warnf("in sync_committee for epoch %d validator %d has bad validator index: %q", epochStats.Epoch, i, valIndexStr) + continue + } + epochStats.syncAssignments[i] = valIndexU64 } - epochStats.syncAssignments[i] = valIndexU64 } } } diff --git a/indexer/indexer.go b/indexer/indexer.go index 4aac1d4f..219f9e2f 100644 --- a/indexer/indexer.go +++ b/indexer/indexer.go @@ -157,8 +157,8 @@ func (indexer *Indexer) GetCachedGenesis() *rpctypes.StandardV1GenesisResponse { return indexer.indexerCache.genesisResp } -func (indexer *Indexer) GetFinalizedEpoch() (int64, []byte) { - return indexer.indexerCache.getFinalizedHead() +func (indexer *Indexer) GetFinalizationCheckpoints() (int64, []byte, int64, []byte) { + return indexer.indexerCache.getFinalizationCheckpoints() } func (indexer *Indexer) GetHighestSlot() uint64 { diff --git a/indexer/synchronizer.go b/indexer/synchronizer.go index 2c6f9f17..2c55abd5 100644 --- a/indexer/synchronizer.go +++ b/indexer/synchronizer.go @@ -98,7 +98,7 @@ func (sync *synchronizerState) runSync() { } retryCount = 0 skipClients = nil - finalizedEpoch, _ := sync.indexer.indexerCache.getFinalizedHead() + finalizedEpoch, _, _, _ := sync.indexer.indexerCache.getFinalizationCheckpoints() sync.stateMutex.Lock() syncEpoch++ sync.currentEpoch = syncEpoch diff --git a/rpc/beaconapi.go b/rpc/beaconapi.go index 5dd9d2b9..fc1fbcb3 100644 --- a/rpc/beaconapi.go +++ b/rpc/beaconapi.go @@ -226,22 +226,17 @@ func (bc *BeaconClient) GetLatestBlockHead() (*rpctypes.StandardV1BeaconHeaderRe return &parsedHeaders, nil } -func (bc *BeaconClient) GetFinalizedBlockHead() (*rpctypes.StandardV1BeaconHeaderResponse, error) { - resHeaders, err := bc.get(fmt.Sprintf("%s/eth/v1/beacon/headers/finalized", bc.endpoint)) +func (bc *BeaconClient) GetFinalityCheckpoints() (*rpctypes.StandardV1BeaconStateFinalityCheckpointsResponse, error) { + var parsedCheckpoints rpctypes.StandardV1BeaconStateFinalityCheckpointsResponse + err := bc.getJson(fmt.Sprintf("%s/eth/v1/beacon/states/head/finality_checkpoints", bc.endpoint), &parsedCheckpoints) if err != nil { if err == errNotFound { // no block found return nil, nil } - return nil, fmt.Errorf("error retrieving finalized block header: %v", err) + return nil, fmt.Errorf("error retrieving finality checkpoints: %v", err) } - - var parsedHeaders rpctypes.StandardV1BeaconHeaderResponse - err = json.Unmarshal(resHeaders, &parsedHeaders) - if err != nil { - return nil, fmt.Errorf("error parsing header-response for finalized block: %v", err) - } - return &parsedHeaders, nil + return &parsedCheckpoints, nil } func (bc *BeaconClient) GetBlockHeaderByBlockroot(blockroot []byte) (*rpctypes.StandardV1BeaconHeaderResponse, error) { diff --git a/rpctypes/beaconapi.go b/rpctypes/beaconapi.go index cb99b199..ff0f26ce 100644 --- a/rpctypes/beaconapi.go +++ b/rpctypes/beaconapi.go @@ -116,3 +116,12 @@ type StandardV1NodeVersionResponse struct { Version string `json:"version"` } `json:"data"` } + +type StandardV1BeaconStateFinalityCheckpointsResponse struct { + Finalized bool `json:"finalized"` + Data struct { + PreviousJustified Checkpoint `json:"previous_justified"` + CurrentJustified Checkpoint `json:"current_justified"` + Finalized Checkpoint `json:"finalized"` + } `json:"data"` +} diff --git a/services/beaconservice.go b/services/beaconservice.go index 50e21fe6..e6e9aa64 100644 --- a/services/beaconservice.go +++ b/services/beaconservice.go @@ -77,7 +77,8 @@ func (bs *BeaconService) GetCachedValidatorSet() *rpctypes.StandardV1StateValida } func (bs *BeaconService) GetFinalizedEpoch() (int64, []byte) { - return bs.indexer.GetFinalizedEpoch() + finalizedEpoch, finalizedRoot, _, _ := bs.indexer.GetFinalizationCheckpoints() + return finalizedEpoch, finalizedRoot } func (bs *BeaconService) GetCachedEpochStats(epoch uint64) *indexer.EpochStats { @@ -253,7 +254,7 @@ func (bs *BeaconService) GetOrphanedBlock(blockroot []byte) *rpctypes.CombinedBl } func (bs *BeaconService) GetEpochAssignments(epoch uint64) (*rpctypes.EpochAssignments, error) { - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() if int64(epoch) > finalizedEpoch { epochStats := bs.indexer.GetCachedEpochStats(epoch) @@ -301,7 +302,7 @@ func (bs *BeaconService) GetProposerAssignments(firstEpoch uint64, lastEpoch uin proposerAssignments = make(map[uint64]uint64) synchronizedEpochs = make(map[uint64]bool) - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() idxMinEpoch := finalizedEpoch + 1 idxHeadEpoch := utils.EpochOfSlot(bs.indexer.GetHighestSlot()) if firstEpoch > idxHeadEpoch { @@ -349,7 +350,7 @@ func (bs *BeaconService) GetDbEpochs(firstEpoch uint64, limit uint32) []*dbtypes dbIdx := 0 dbCnt := len(dbEpochs) - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() var idxMinEpoch, idxHeadEpoch uint64 idxMinEpoch = uint64(finalizedEpoch + 1) idxHeadEpoch = utils.EpochOfSlot(bs.indexer.GetHighestSlot()) @@ -381,7 +382,7 @@ func (bs *BeaconService) GetDbBlocks(firstSlot uint64, limit int32, withOrphaned resBlocks := make([]*dbtypes.Block, limit) resIdx := 0 - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() idxMinSlot := (finalizedEpoch + 1) * int64(utils.Config.Chain.Config.SlotsPerEpoch) idxHeadSlot := bs.indexer.GetHighestSlot() if firstSlot > idxHeadSlot { @@ -429,7 +430,7 @@ func (bs *BeaconService) GetDbBlocks(firstSlot uint64, limit int32, withOrphaned func (bs *BeaconService) GetDbBlocksForSlots(firstSlot uint64, slotLimit uint32, withOrphaned bool) []*dbtypes.Block { resBlocks := make([]*dbtypes.Block, 0) - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() idxMinSlot := (finalizedEpoch + 1) * int64(utils.Config.Chain.Config.SlotsPerEpoch) idxHeadSlot := bs.indexer.GetHighestSlot() if firstSlot > idxHeadSlot { @@ -485,7 +486,7 @@ type cachedDbBlock struct { func (bs *BeaconService) GetDbBlocksByFilter(filter *dbtypes.BlockFilter, pageIdx uint64, pageSize uint32) []*dbtypes.AssignedBlock { cachedMatches := make([]cachedDbBlock, 0) - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() idxMinSlot := (finalizedEpoch + 1) * int64(utils.Config.Chain.Config.SlotsPerEpoch) idxHeadSlot := bs.indexer.GetHighestSlot() proposedMap := map[uint64]bool{} @@ -673,7 +674,7 @@ func (bs *BeaconService) GetValidatorActivity() (map[uint64]uint8, uint64) { return activityMap, 0 } idxHeadEpoch-- - finalizedEpoch, _ := bs.indexer.GetFinalizedEpoch() + finalizedEpoch, _ := bs.GetFinalizedEpoch() var idxMinEpoch uint64 if finalizedEpoch < 0 { idxMinEpoch = 0 diff --git a/templates/epochs/epochs.html b/templates/epochs/epochs.html index 46820ad9..0c64adf2 100644 --- a/templates/epochs/epochs.html +++ b/templates/epochs/epochs.html @@ -86,6 +86,8 @@

{{ if $epoch.Finalized }} Yes + {{ else if $epoch.Justified }} + Just {{ else }} No {{ end }} diff --git a/templates/index/recentEpochs.html b/templates/index/recentEpochs.html index ea9d6cfa..c77eec7a 100644 --- a/templates/index/recentEpochs.html +++ b/templates/index/recentEpochs.html @@ -27,7 +27,8 @@

Yes - No + Just. + No @@ -55,6 +56,8 @@
Yes + {{ else if $epoch.Justified }} + Just {{ else }} No {{ end }} diff --git a/types/models/epochs.go b/types/models/epochs.go index a36349d7..f6624b44 100644 --- a/types/models/epochs.go +++ b/types/models/epochs.go @@ -27,6 +27,7 @@ type EpochsPageDataEpoch struct { Epoch uint64 `json:"epoch"` Ts time.Time `json:"ts"` Finalized bool `json:"finalized"` + Justified bool `json:"justified"` Synchronized bool `json:"synchronized"` CanonicalBlockCount uint64 `json:"canonical_block_count"` OrphanedBlockCount uint64 `json:"orphaned_block_count"` diff --git a/types/models/indexPage.go b/types/models/indexPage.go index c1aacf03..c0a64898 100644 --- a/types/models/indexPage.go +++ b/types/models/indexPage.go @@ -12,6 +12,7 @@ type IndexPageData struct { SlotsPerEpoch uint64 `json:"slots_per_epoch"` CurrentEpoch uint64 `json:"cur_epoch"` CurrentFinalizedEpoch int64 `json:"finalized_epoch"` + CurrentJustifiedEpoch int64 `json:"justified_epoch"` CurrentSlot uint64 `json:"cur_slot"` CurrentScheduledCount uint64 `json:"cur_scheduled"` CurrentEpochProgress float64 `json:"cur_epoch_prog"` @@ -48,6 +49,7 @@ type IndexPageDataEpochs struct { Epoch uint64 `json:"epoch"` Ts time.Time `json:"ts"` Finalized bool `json:"finalized"` + Justified bool `json:"justified"` EligibleEther uint64 `json:"eligible"` TargetVoted uint64 `json:"voted"` VoteParticipation float64 `json:"votep"`