From 904df1121c7c58d3a731c15bdcca4514ef0fbb2f Mon Sep 17 00:00:00 2001 From: pk910 Date: Tue, 11 Feb 2025 16:49:59 +0100 Subject: [PATCH 1/8] basic eip7805 support (FOCIL) --- clients/consensus/chainspec.go | 2 ++ go.mod | 2 ++ go.sum | 4 ++-- handlers/index.go | 8 ++++++++ handlers/slot.go | 22 +++++++++++++++++++++ indexer/beacon/block_helper.go | 36 ++++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 2 deletions(-) diff --git a/clients/consensus/chainspec.go b/clients/consensus/chainspec.go index 3bb2b347..f190f795 100644 --- a/clients/consensus/chainspec.go +++ b/clients/consensus/chainspec.go @@ -36,6 +36,8 @@ type ChainSpec struct { ElectraForkEpoch *uint64 `yaml:"ELECTRA_FORK_EPOCH"` Eip7594ForkVersion phase0.Version `yaml:"EIP7594_FORK_VERSION" check-if-fork:"Eip7594ForkEpoch"` Eip7594ForkEpoch *uint64 `yaml:"EIP7594_FORK_EPOCH"` + Eip7805ForkVersion phase0.Version `yaml:"EIP7805_FORK_VERSION" check-if-fork:"Eip7805ForkEpoch"` + Eip7805ForkEpoch *uint64 `yaml:"EIP7805_FORK_EPOCH"` SecondsPerSlot time.Duration `yaml:"SECONDS_PER_SLOT"` SlotsPerEpoch uint64 `yaml:"SLOTS_PER_EPOCH"` EpochsPerHistoricalVector uint64 `yaml:"EPOCHS_PER_HISTORICAL_VECTOR"` diff --git a/go.mod b/go.mod index 31f352f8..34f5b829 100644 --- a/go.mod +++ b/go.mod @@ -153,4 +153,6 @@ require ( rsc.io/tmplfunc v0.0.3 // indirect ) +replace github.com/attestantio/go-eth2-client => github.com/pk910/go-eth2-client v0.0.0-20250211153905-1124ee6e3b00 + replace github.com/ethereum/go-ethereum => github.com/s1na/go-ethereum v0.0.0-20250103133732-7e1b0ba7e83f diff --git a/go.sum b/go.sum index 8e7fc5a5..72afcd14 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= -github.com/attestantio/go-eth2-client v0.24.0 h1:lGVbcnhlBwRglt1Zs56JOCgXVyLWKFZOmZN8jKhE7Ws= -github.com/attestantio/go-eth2-client v0.24.0/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= @@ -351,6 +349,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pk910/dynamic-ssz v0.0.5 h1:VP9heGYUwzlpyhk28P2nCAzhvGsePJOOOO5vQMDh2qQ= github.com/pk910/dynamic-ssz v0.0.5/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pk910/go-eth2-client v0.0.0-20250211153905-1124ee6e3b00 h1:UQPOhVdHkvhICZhBqt6YTCTXUthZ7Wp69EOch1ChdbA= +github.com/pk910/go-eth2-client v0.0.0-20250211153905-1124ee6e3b00/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/handlers/index.go b/handlers/index.go index 1005ef35..ce8bcec0 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -216,6 +216,14 @@ func buildIndexPageData() (*models.IndexPageData, time.Duration) { Active: uint64(currentEpoch) >= *specs.Eip7594ForkEpoch, }) } + if specs.Eip7805ForkEpoch != nil && *specs.Eip7805ForkEpoch < uint64(18446744073709551615) { + pageData.NetworkForks = append(pageData.NetworkForks, &models.IndexPageDataForks{ + Name: "eip7805", + Epoch: *specs.Eip7805ForkEpoch, + Version: specs.Eip7805ForkVersion[:], + Active: uint64(currentEpoch) >= *specs.Eip7805ForkEpoch, + }) + } // load recent epochs buildIndexPageRecentEpochsData(pageData, currentEpoch, finalizedEpoch, justifiedEpoch, recentEpochCount) diff --git a/handlers/slot.go b/handlers/slot.go index c396785c..71e09dfe 100644 --- a/handlers/slot.go +++ b/handlers/slot.go @@ -688,6 +688,28 @@ func getSlotPageBlockData(blockData *services.CombinedBlockResponse, epochStatsV BlockNumber: uint64(executionPayload.BlockNumber), } getSlotPageTransactions(pageData, executionPayload.Transactions) + case spec.DataVersionEip7805: + if blockData.Block.Eip7805 == nil { + break + } + executionPayload := blockData.Block.Eip7805.Message.Body.ExecutionPayload + pageData.ExecutionData = &models.SlotPageExecutionData{ + ParentHash: executionPayload.ParentHash[:], + FeeRecipient: executionPayload.FeeRecipient[:], + StateRoot: executionPayload.StateRoot[:], + ReceiptsRoot: executionPayload.ReceiptsRoot[:], + LogsBloom: executionPayload.LogsBloom[:], + Random: executionPayload.PrevRandao[:], + GasLimit: uint64(executionPayload.GasLimit), + GasUsed: uint64(executionPayload.GasUsed), + Timestamp: uint64(executionPayload.Timestamp), + Time: time.Unix(int64(executionPayload.Timestamp), 0), + ExtraData: executionPayload.ExtraData, + BaseFeePerGas: executionPayload.BaseFeePerGas.Uint64(), + BlockHash: executionPayload.BlockHash[:], + BlockNumber: uint64(executionPayload.BlockNumber), + } + getSlotPageTransactions(pageData, executionPayload.Transactions) } } diff --git a/indexer/beacon/block_helper.go b/indexer/beacon/block_helper.go index 38472966..5751297b 100644 --- a/indexer/beacon/block_helper.go +++ b/indexer/beacon/block_helper.go @@ -44,6 +44,9 @@ func MarshalVersionedSignedBeaconBlockSSZ(dynSsz *dynssz.DynSsz, block *spec.Ver case spec.DataVersionElectra: version = uint64(block.Version) ssz, err = dynSsz.MarshalSSZ(block.Electra) + case spec.DataVersionEip7805: + version = uint64(block.Version) + ssz, err = dynSsz.MarshalSSZ(block.Eip7805) default: err = fmt.Errorf("unknown block version") } @@ -110,6 +113,11 @@ func unmarshalVersionedSignedBeaconBlockSSZ(dynSsz *dynssz.DynSsz, version uint6 if err := dynSsz.UnmarshalSSZ(block.Electra, ssz); err != nil { return nil, fmt.Errorf("failed to decode electra signed beacon block: %v", err) } + case spec.DataVersionEip7805: + block.Eip7805 = &electra.SignedBeaconBlock{} + if err := dynSsz.UnmarshalSSZ(block.Eip7805, ssz); err != nil { + return nil, fmt.Errorf("failed to decode eip7805 signed beacon block: %v", err) + } default: return nil, fmt.Errorf("unknown block version") } @@ -137,6 +145,9 @@ func MarshalVersionedSignedBeaconBlockJson(block *spec.VersionedSignedBeaconBloc case spec.DataVersionElectra: version = uint64(block.Version) jsonRes, err = block.Electra.MarshalJSON() + case spec.DataVersionEip7805: + version = uint64(block.Version) + jsonRes, err = block.Eip7805.MarshalJSON() default: err = fmt.Errorf("unknown block version") } @@ -185,6 +196,11 @@ func unmarshalVersionedSignedBeaconBlockJson(version uint64, ssz []byte) (*spec. if err := block.Electra.UnmarshalJSON(ssz); err != nil { return nil, fmt.Errorf("failed to decode electra signed beacon block: %v", err) } + case spec.DataVersionEip7805: + block.Eip7805 = &electra.SignedBeaconBlock{} + if err := block.Eip7805.UnmarshalJSON(ssz); err != nil { + return nil, fmt.Errorf("failed to decode eip7805 signed beacon block: %v", err) + } default: return nil, fmt.Errorf("unknown block version") } @@ -218,6 +234,12 @@ func getBlockExecutionExtraData(v *spec.VersionedSignedBeaconBlock) ([]byte, err } return v.Electra.Message.Body.ExecutionPayload.ExtraData, nil + case spec.DataVersionEip7805: + if v.Eip7805 == nil || v.Eip7805.Message == nil || v.Eip7805.Message.Body == nil || v.Eip7805.Message.Body.ExecutionPayload == nil { + return nil, errors.New("no eip7805 block") + } + + return v.Eip7805.Message.Body.ExecutionPayload.ExtraData, nil default: return nil, errors.New("unknown version") } @@ -262,6 +284,12 @@ func getStateRandaoMixes(v *spec.VersionedBeaconState) ([]phase0.Root, error) { } return v.Electra.RANDAOMixes, nil + case spec.DataVersionEip7805: + if v.Eip7805 == nil || v.Eip7805.RANDAOMixes == nil { + return nil, errors.New("no eip7805 block") + } + + return v.Eip7805.RANDAOMixes, nil default: return nil, errors.New("unknown version") } @@ -282,6 +310,8 @@ func getStateDepositIndex(state *spec.VersionedBeaconState) uint64 { return state.Deneb.ETH1DepositIndex case spec.DataVersionElectra: return state.Electra.ETH1DepositIndex + case spec.DataVersionEip7805: + return state.Eip7805.ETH1DepositIndex } return 0 } @@ -321,6 +351,12 @@ func getStateCurrentSyncCommittee(v *spec.VersionedBeaconState) ([]phase0.BLSPub } return v.Electra.CurrentSyncCommittee.Pubkeys, nil + case spec.DataVersionEip7805: + if v.Eip7805 == nil || v.Eip7805.CurrentSyncCommittee == nil { + return nil, errors.New("no eip7805 block") + } + + return v.Eip7805.CurrentSyncCommittee.Pubkeys, nil default: return nil, errors.New("unknown version") } From cac66d7dffed7674bc1d0a22f3a1c82888014839 Mon Sep 17 00:00:00 2001 From: pk910 Date: Tue, 11 Feb 2025 16:53:42 +0100 Subject: [PATCH 2/8] trigger CI From e1cd242f27eff9c9cf63ace5a489b496d169ac4a Mon Sep 17 00:00:00 2001 From: pk910 Date: Tue, 11 Feb 2025 21:50:00 +0100 Subject: [PATCH 3/8] bump go-eth2-client --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 34f5b829..2156bc0c 100644 --- a/go.mod +++ b/go.mod @@ -153,6 +153,6 @@ require ( rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/attestantio/go-eth2-client => github.com/pk910/go-eth2-client v0.0.0-20250211153905-1124ee6e3b00 +replace github.com/attestantio/go-eth2-client => github.com/pk910/go-eth2-client v0.0.0-20250211203818-c8302f9d7ff1 replace github.com/ethereum/go-ethereum => github.com/s1na/go-ethereum v0.0.0-20250103133732-7e1b0ba7e83f diff --git a/go.sum b/go.sum index 72afcd14..95dc276c 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pk910/dynamic-ssz v0.0.5 h1:VP9heGYUwzlpyhk28P2nCAzhvGsePJOOOO5vQMDh2qQ= github.com/pk910/dynamic-ssz v0.0.5/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= -github.com/pk910/go-eth2-client v0.0.0-20250211153905-1124ee6e3b00 h1:UQPOhVdHkvhICZhBqt6YTCTXUthZ7Wp69EOch1ChdbA= -github.com/pk910/go-eth2-client v0.0.0-20250211153905-1124ee6e3b00/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o= +github.com/pk910/go-eth2-client v0.0.0-20250211203818-c8302f9d7ff1 h1:MjDKpG5tqsuzBWRAiY2jhzrNuTD5U3uBvQm/stXr2vs= +github.com/pk910/go-eth2-client v0.0.0-20250211203818-c8302f9d7ff1/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 0718482ec0a1e659edf74166b62d4ff8d37f65d3 Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Tue, 11 Mar 2025 19:37:53 +0900 Subject: [PATCH 4/8] Display ILs per slot --- clients/consensus/client.go | 5 + clients/consensus/clientlogic.go | 14 +- clients/consensus/rpc/beaconstream.go | 34 ++++- handlers/slot.go | 180 ++++++++++++++++---------- indexer/beacon/client.go | 17 ++- indexer/beacon/inclusionlistcache.go | 104 +++++++++++++++ indexer/beacon/indexer.go | 14 +- indexer/beacon/indexer_getter.go | 5 + templates/slot/inclusion_lists.html | 102 +++++++++++++++ templates/slot/slot.html | 20 +++ types/models/slot.go | 37 ++++-- 11 files changed, 437 insertions(+), 95 deletions(-) create mode 100644 indexer/beacon/inclusionlistcache.go create mode 100644 templates/slot/inclusion_lists.html diff --git a/clients/consensus/client.go b/clients/consensus/client.go index 013ac9d8..61ab4e96 100644 --- a/clients/consensus/client.go +++ b/clients/consensus/client.go @@ -52,6 +52,7 @@ type Client struct { blockDispatcher Dispatcher[*v1.BlockEvent] headDispatcher Dispatcher[*v1.HeadEvent] checkpointDispatcher Dispatcher[*v1.Finality] + inclusionListDispatcher Dispatcher[*v1.InclusionListEvent] } func (pool *Pool) newPoolClient(clientIdx uint16, endpoint *ClientConfig) (*Client, error) { @@ -96,6 +97,10 @@ func (client *Client) SubscribeFinalizedEvent(capacity int) *Subscription[*v1.Fi return client.checkpointDispatcher.Subscribe(capacity, false) } +func (client *Client) SubscribeInclusionListEvent(capacity int, blocking bool) *Subscription[*v1.InclusionListEvent] { + return client.inclusionListDispatcher.Subscribe(capacity, blocking) +} + func (client *Client) GetPool() *Pool { return client.pool } diff --git a/clients/consensus/clientlogic.go b/clients/consensus/clientlogic.go index f497998f..919a3a06 100644 --- a/clients/consensus/clientlogic.go +++ b/clients/consensus/clientlogic.go @@ -124,7 +124,7 @@ func (client *Client) runClientLogic() error { } // start event stream - blockStream := client.rpcClient.NewBlockStream(client.clientCtx, client.logger, rpc.StreamBlockEvent|rpc.StreamHeadEvent|rpc.StreamFinalizedEvent) + blockStream := client.rpcClient.NewBlockStream(client.clientCtx, client.logger, rpc.StreamBlockEvent|rpc.StreamHeadEvent|rpc.StreamFinalizedEvent|rpc.StreamInclusionListEvent) defer blockStream.Close() // process events @@ -162,6 +162,12 @@ func (client *Client) runClientLogic() error { if err != nil { client.logger.Warnf("failed processing finalized event: %v", err) } + + case rpc.StreamInclusionListEvent: + err := client.processInclusionListEvent(evt.Data.(*v1.InclusionListEvent)) + if err != nil { + client.logger.Warnf("failed processing inclusion list event: %v", err) + } } client.logger.Tracef("event (%v) processing time: %v ms", evt.Event, time.Since(now).Milliseconds()) @@ -352,6 +358,12 @@ func (client *Client) processFinalizedEvent(evt *v1.FinalizedCheckpointEvent) er return nil } +func (client *Client) processInclusionListEvent(evt *v1.InclusionListEvent) error { + client.inclusionListDispatcher.Fire(evt) + + return nil +} + func (client *Client) pollClientHead() error { ctx, cancel := context.WithTimeout(client.clientCtx, 10*time.Second) defer cancel() diff --git a/clients/consensus/rpc/beaconstream.go b/clients/consensus/rpc/beaconstream.go index be6fd92c..70f978dc 100644 --- a/clients/consensus/rpc/beaconstream.go +++ b/clients/consensus/rpc/beaconstream.go @@ -17,9 +17,10 @@ import ( ) const ( - StreamBlockEvent uint16 = 0x01 - StreamHeadEvent uint16 = 0x02 - StreamFinalizedEvent uint16 = 0x04 + StreamBlockEvent uint16 = 0x01 + StreamHeadEvent uint16 = 0x02 + StreamFinalizedEvent uint16 = 0x04 + StreamInclusionListEvent uint16 = 0x08 ) type BeaconStreamEvent struct { @@ -87,6 +88,8 @@ func (bs *BeaconStream) startStream() { bs.processHeadEvent(evt) case "finalized_checkpoint": bs.processFinalizedEvent(evt) + case "inclusion_list": + bs.processInclusionListEvent(evt) } case <-stream.Ready: bs.ReadyChan <- &BeaconStreamStatus{ @@ -148,6 +151,16 @@ func (bs *BeaconStream) subscribeStream(endpoint string, events uint16) *eventst topicsCount++ } + if events&StreamInclusionListEvent > 0 { + if topicsCount > 0 { + fmt.Fprintf(&topics, ",") + } + + fmt.Fprintf(&topics, "inclusion_list") + + topicsCount++ + } + if topicsCount == 0 { return nil } @@ -225,6 +238,21 @@ func (bs *BeaconStream) processFinalizedEvent(evt eventsource.Event) { } } +func (bs *BeaconStream) processInclusionListEvent(evt eventsource.Event) { + var parsed v1.InclusionListEvent + + err := json.Unmarshal([]byte(evt.Data()), &parsed) + if err != nil { + bs.logger.Warnf("beacon block stream failed to decode inclusion_list event: %v", err) + return + } + + bs.EventChan <- &BeaconStreamEvent{ + Event: StreamInclusionListEvent, + Data: &parsed, + } +} + func getRedactedURL(requrl string) string { var logurl string diff --git a/handlers/slot.go b/handlers/slot.go index c593484d..55ca2241 100644 --- a/handlers/slot.go +++ b/handlers/slot.go @@ -37,6 +37,7 @@ func Slot(w http.ResponseWriter, r *http.Request) { var slotTemplateFiles = append(layoutTemplateFiles, "slot/slot.html", "slot/overview.html", + "slot/inclusion_lists.html", "slot/transactions.html", "slot/attestations.html", "slot/deposits.html", @@ -294,6 +295,9 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) ( } } + pageData.InclusionLists = getSlotPageInclusionLists(slot, pageData.Block.Transactions) + pageData.InclusionListsCount = uint64(len(pageData.InclusionLists)) + return pageData, cacheTimeout } @@ -763,82 +767,39 @@ func getSlotPageBlockData(blockData *services.CombinedBlockResponse, epochStatsV return pageData } -func getSlotPageTransactions(pageData *models.SlotPageBlockData, transactions []bellatrix.Transaction) { - pageData.Transactions = make([]*models.SlotPageTransaction, 0) - sigLookupBytes := []types.TxSignatureBytes{} - sigLookupMap := map[types.TxSignatureBytes][]*models.SlotPageTransaction{} - - for idx, txBytes := range transactions { - var tx ethtypes.Transaction +func getSlotPageInclusionLists(slot phase0.Slot, blockTransactions []*models.SlotPageTransaction) []*models.SlotPageInclusionList { + blockTransactionMap := map[string]bool{} + for _, transaction := range blockTransactions { + blockTransactionMap[string(transaction.Hash)] = true + } - err := tx.UnmarshalBinary(txBytes) - if err != nil { - logrus.Warnf("error decoding transaction 0x%x.%v: %v\n", pageData.BlockRoot, idx, err) - continue + inclusionLists := []*models.SlotPageInclusionList{} + for _, inclusionList := range services.GlobalBeaconService.GetBeaconIndexer().GetInclusionListsBySlot(slot - 1) { + transactions := decodeTransactions(nil, inclusionList.Message.Transactions) + transactionsIncluded := make([]bool, len(transactions)) + for i, transaction := range transactions { + transactionsIncluded[i] = blockTransactionMap[string(transaction.Hash)] } - txHash := tx.Hash() - txValue, _ := tx.Value().Float64() - ethFloat, _ := utils.ETH.Float64() - txValue = txValue / ethFloat - - txData := &models.SlotPageTransaction{ - Index: uint64(idx), - Hash: txHash[:], - Value: txValue, - Data: tx.Data(), - Type: uint64(tx.Type()), - } - txData.DataLen = uint64(len(txData.Data)) - txFrom, err := ethtypes.Sender(ethtypes.NewPragueSigner(tx.ChainId()), &tx) - if err != nil { - txData.From = "unknown" - logrus.Warnf("error decoding transaction sender 0x%x.%v: %v\n", pageData.BlockRoot, idx, err) - } else { - txData.From = txFrom.String() - } - txTo := tx.To() - if txTo == nil { - txData.To = "new contract" - } else { - txData.To = txTo.String() - } + inclusionLists = append(inclusionLists, &models.SlotPageInclusionList{ + Validator: types.NamedValidator{ + Index: uint64(inclusionList.Message.ValidatorIndex), + Name: services.GlobalBeaconService.GetValidatorName(uint64(inclusionList.Message.ValidatorIndex)), + }, + InclusionListCommitteeRoot: inclusionList.Message.InclusionListCommitteeRoot[:], + Transactions: transactions, + TransactionsCount: uint64(len(transactions)), + TransactionsIncluded: transactionsIncluded, + Signature: inclusionList.Signature[:], + }) + } - pageData.Transactions = append(pageData.Transactions, txData) + return inclusionLists +} - // check call fn signature - if txData.DataLen >= 4 { - sigBytes := types.TxSignatureBytes(txData.Data[0:4]) - if sigLookupMap[sigBytes] == nil { - sigLookupMap[sigBytes] = []*models.SlotPageTransaction{ - txData, - } - sigLookupBytes = append(sigLookupBytes, sigBytes) - } else { - sigLookupMap[sigBytes] = append(sigLookupMap[sigBytes], txData) - } - } else { - txData.FuncSigStatus = 10 - txData.FuncName = "transfer" - } - } +func getSlotPageTransactions(pageData *models.SlotPageBlockData, transactions []bellatrix.Transaction) { + pageData.Transactions = decodeTransactions(pageData.BlockRoot, transactions) pageData.TransactionsCount = uint64(len(transactions)) - - if len(sigLookupBytes) > 0 { - sigLookups := services.GlobalTxSignaturesService.LookupSignatures(sigLookupBytes) - for _, sigLookup := range sigLookups { - for _, txData := range sigLookupMap[sigLookup.Bytes] { - txData.FuncSigStatus = uint64(sigLookup.Status) - txData.FuncBytes = fmt.Sprintf("0x%x", sigLookup.Bytes[:]) - if sigLookup.Status == types.TxSigStatusFound { - txData.FuncSig = sigLookup.Signature - txData.FuncName = sigLookup.Name - } else { - txData.FuncName = "call?" - } - } - } - } } func getSlotPageDepositRequests(pageData *models.SlotPageBlockData, depositRequests []*electra.DepositRequest) { @@ -984,3 +945,82 @@ func handleSlotDownload(ctx context.Context, w http.ResponseWriter, blockSlot in return fmt.Errorf("unknown download type: %s", downloadType) } } + +func decodeTransactions(blockRoot []byte, transactions []bellatrix.Transaction) []*models.SlotPageTransaction { + decodedTransactions := make([]*models.SlotPageTransaction, 0) + sigLookupBytes := []types.TxSignatureBytes{} + sigLookupMap := map[types.TxSignatureBytes][]*models.SlotPageTransaction{} + + for idx, txBytes := range transactions { + var tx ethtypes.Transaction + + err := tx.UnmarshalBinary(txBytes) + if err != nil { + logrus.Warnf("error decoding transaction 0x%x.%v: %v\n", blockRoot, idx, err) + continue + } + + txHash := tx.Hash() + txValue, _ := tx.Value().Float64() + ethFloat, _ := utils.ETH.Float64() + txValue = txValue / ethFloat + + txData := &models.SlotPageTransaction{ + Index: uint64(idx), + Hash: txHash[:], + Value: txValue, + Data: tx.Data(), + Type: uint64(tx.Type()), + } + txData.DataLen = uint64(len(txData.Data)) + txFrom, err := ethtypes.Sender(ethtypes.NewPragueSigner(tx.ChainId()), &tx) + if err != nil { + txData.From = "unknown" + logrus.Warnf("error decoding transaction sender 0x%x.%v: %v\n", blockRoot, idx, err) + } else { + txData.From = txFrom.String() + } + txTo := tx.To() + if txTo == nil { + txData.To = "new contract" + } else { + txData.To = txTo.String() + } + + decodedTransactions = append(decodedTransactions, txData) + + // check call fn signature + if txData.DataLen >= 4 { + sigBytes := types.TxSignatureBytes(txData.Data[0:4]) + if sigLookupMap[sigBytes] == nil { + sigLookupMap[sigBytes] = []*models.SlotPageTransaction{ + txData, + } + sigLookupBytes = append(sigLookupBytes, sigBytes) + } else { + sigLookupMap[sigBytes] = append(sigLookupMap[sigBytes], txData) + } + } else { + txData.FuncSigStatus = 10 + txData.FuncName = "transfer" + } + } + + if len(sigLookupBytes) > 0 { + sigLookups := services.GlobalTxSignaturesService.LookupSignatures(sigLookupBytes) + for _, sigLookup := range sigLookups { + for _, txData := range sigLookupMap[sigLookup.Bytes] { + txData.FuncSigStatus = uint64(sigLookup.Status) + txData.FuncBytes = fmt.Sprintf("0x%x", sigLookup.Bytes[:]) + if sigLookup.Status == types.TxSigStatusFound { + txData.FuncSig = sigLookup.Signature + txData.FuncName = sigLookup.Name + } else { + txData.FuncName = "call?" + } + } + } + } + + return decodedTransactions +} diff --git a/indexer/beacon/client.go b/indexer/beacon/client.go index c3733051..32a2b58e 100644 --- a/indexer/beacon/client.go +++ b/indexer/beacon/client.go @@ -30,8 +30,9 @@ type Client struct { archive bool skipValidators bool - blockSubscription *consensus.Subscription[*v1.BlockEvent] - headSubscription *consensus.Subscription[*v1.HeadEvent] + blockSubscription *consensus.Subscription[*v1.BlockEvent] + headSubscription *consensus.Subscription[*v1.HeadEvent] + inclusionListSubscription *consensus.Subscription[*v1.InclusionListEvent] headRoot phase0.Root } @@ -79,6 +80,7 @@ func (c *Client) startIndexing() { // blocking block subscription with a buffer to ensure no blocks are missed c.blockSubscription = c.client.SubscribeBlockEvent(100, true) c.headSubscription = c.client.SubscribeHeadEvent(100, true) + c.inclusionListSubscription = c.client.SubscribeInclusionListEvent(100, true) go c.startClientLoop() } @@ -177,6 +179,11 @@ func (c *Client) runClientLoop() error { if err != nil { c.logger.Errorf("failed processing head %v (%v): %v", headEvent.Slot, headEvent.Block.String(), err) } + case inclusionListEvent := <-c.inclusionListSubscription.Channel(): + err := c.processInclusionListEvent(inclusionListEvent) + if err != nil { + c.logger.Errorf("failed processing inclusion list %v (%v): %v", inclusionListEvent.Data.Message.Slot, inclusionListEvent.Data.Message.ValidatorIndex, err) + } } } @@ -284,6 +291,12 @@ func (c *Client) processHeadEvent(headEvent *v1.HeadEvent) error { return nil } +func (c *Client) processInclusionListEvent(inclusionListEvent *v1.InclusionListEvent) error { + c.indexer.inclusionListCache.addInclusionList(inclusionListEvent.Data) + + return nil +} + // processStreamBlock processes a block received from the stream (either via block or head events). func (c *Client) processStreamBlock(slot phase0.Slot, root phase0.Root) (*Block, error) { block, isNew, processingTimes, err := c.processBlock(slot, root, nil) diff --git a/indexer/beacon/inclusionlistcache.go b/indexer/beacon/inclusionlistcache.go new file mode 100644 index 00000000..d667a963 --- /dev/null +++ b/indexer/beacon/inclusionlistcache.go @@ -0,0 +1,104 @@ +package beacon + +import ( + "runtime/debug" + "sync" + "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// inclusionListCache is a cache for storing inclusion lists. +type inclusionListCache struct { + indexer *Indexer + cacheMutex sync.RWMutex + inclusionListMap map[phase0.Slot][]*v1.SignedInclusionList +} + +// newInclusionListCache creates a new instance of inclusionListCache. +func newInclusionListCache(indexer *Indexer) *inclusionListCache { + cache := &inclusionListCache{ + indexer: indexer, + inclusionListMap: map[phase0.Slot][]*v1.SignedInclusionList{}, + } + + go cache.cleanupLoop() + + return cache +} + +// addInclusionList adds the given inclusion list to the cache. +func (cache *inclusionListCache) addInclusionList(inclusionList *v1.SignedInclusionList) { + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + + for _, inclusionListCached := range cache.inclusionListMap[inclusionList.Message.Slot] { + if inclusionListCached.Message.ValidatorIndex != inclusionList.Message.ValidatorIndex { + continue + } + + if inclusionListCached.Signature == inclusionList.Signature { + // This is a duplicated event possibly coming from different clients. + return + } + + // This is an equivocation. We cache all of an equivocator's inclusion lists to display them in the explorer. + break + } + + cache.inclusionListMap[inclusionList.Message.Slot] = append(cache.inclusionListMap[inclusionList.Message.Slot], inclusionList) +} + +// getInclusionListsBySlot returns the cached inclusion lists with the given slot. +func (cache *inclusionListCache) getInclusionListsBySlot(slot phase0.Slot) []*v1.SignedInclusionList { + cache.cacheMutex.RLock() + defer cache.cacheMutex.RUnlock() + + inclusionLists := make([]*v1.SignedInclusionList, len(cache.inclusionListMap[slot])) + if len(inclusionLists) > 0 { + copy(inclusionLists, cache.inclusionListMap[slot]) + } + + return inclusionLists +} + +// cleanupLoop is a loop that cleans up the cache. +func (cache *inclusionListCache) cleanupLoop() { + defer func() { + if err := recover(); err != nil { + cache.indexer.logger.WithError(err.(error)).Errorf("uncaught panic in indexer.beacon.inclusionListCache.cleanupLoop subroutine: %v, stack: %v", err, string(debug.Stack())) + time.Sleep(10 * time.Second) + + go cache.cleanupLoop() + } + }() + + for { + time.Sleep(30 * time.Minute) + cache.cleanupCache() + } +} + +// cleanupCache cleans up the cache. +func (cache *inclusionListCache) cleanupCache() { + chainState := cache.indexer.consensusPool.GetChainState() + cutOffEpoch := chainState.CurrentEpoch() - phase0.Epoch(cache.indexer.activityHistoryLength) + if cutOffEpoch < 0 { + return + } + + cache.cacheMutex.Lock() + defer cache.cacheMutex.Unlock() + + deleted := 0 + for slot, inclusionLists := range cache.inclusionListMap { + if chainState.EpochOfSlot(slot) < cutOffEpoch { + deleted += len(inclusionLists) + delete(cache.inclusionListMap, slot) + continue + } + } + + cache.indexer.logger.Infof("cleaned up inclusion list cache, deleted %d entries", deleted) +} diff --git a/indexer/beacon/indexer.go b/indexer/beacon/indexer.go index b3d9b49a..977f4d63 100644 --- a/indexer/beacon/indexer.go +++ b/indexer/beacon/indexer.go @@ -38,12 +38,13 @@ type Indexer struct { maxParallelStateCalls uint16 // caches - blockCache *blockCache - epochCache *epochCache - forkCache *forkCache - pubkeyCache *pubkeyCache - validatorCache *validatorCache - validatorActivity *validatorActivityCache + blockCache *blockCache + inclusionListCache *inclusionListCache + epochCache *epochCache + forkCache *forkCache + pubkeyCache *pubkeyCache + validatorCache *validatorCache + validatorActivity *validatorActivityCache // indexer state clients []*Client @@ -102,6 +103,7 @@ func NewIndexer(logger logrus.FieldLogger, consensusPool *consensus.Pool) *Index } indexer.blockCache = newBlockCache(indexer) + indexer.inclusionListCache = newInclusionListCache(indexer) indexer.epochCache = newEpochCache(indexer) indexer.forkCache = newForkCache(indexer) indexer.pubkeyCache = newPubkeyCache(indexer, utils.Config.Indexer.PubkeyCachePath) diff --git a/indexer/beacon/indexer_getter.go b/indexer/beacon/indexer_getter.go index 1bfe512c..2987e476 100644 --- a/indexer/beacon/indexer_getter.go +++ b/indexer/beacon/indexer_getter.go @@ -202,6 +202,11 @@ func (indexer *Indexer) GetOrphanedBlockByRoot(blockRoot phase0.Root) (*Block, e return block, nil } +// GetInclusionListsBySlot returns a slice of inclusion lists with the given slot. +func (indexer *Indexer) GetInclusionListsBySlot(slot phase0.Slot) []*v1.SignedInclusionList { + return indexer.inclusionListCache.getInclusionListsBySlot(slot) +} + // GetEpochStats returns the epoch stats for the given epoch and optional fork ID override. func (indexer *Indexer) GetEpochStats(epoch phase0.Epoch, overrideForkId *ForkKey) *EpochStats { epochStats := indexer.epochCache.getEpochStatsByEpoch(epoch) diff --git a/templates/slot/inclusion_lists.html b/templates/slot/inclusion_lists.html new file mode 100644 index 00000000..803db1bb --- /dev/null +++ b/templates/slot/inclusion_lists.html @@ -0,0 +1,102 @@ +{{ define "inclusion_lists" }} + {{ range $i, $inclusionList := .InclusionLists }} +
+
+
+
Inclusion List {{ $i }}
+
+
+
Validator:
+
+ {{ formatValidatorWithIndex $inclusionList.Validator.Index $inclusionList.Validator.Name }} +
+
+
+
IL Committee Root:
+
+ 0x{{ printf "%x" $inclusionList.InclusionListCommitteeRoot }} + +
+
+
+
Signature:
+
0x{{ printf "%x" $inclusionList.Signature }}
+
+
+
Transactions:
+
{{ formatAddCommas $inclusionList.TransactionsCount }}
+
+
+ + + + + + + + + + + + + + + + {{ range $j, $transaction := $inclusionList.Transactions }} + + + + + + + + + + + + {{ end }} + +
#HashFromToMethodValueCall DataIncluded
{{ $j }} +
+ +
+ 0x{{ printf "%x" $transaction.Hash }} +
+
+ +
+ {{ $transaction.From }} +
+
+ +
+ {{ $transaction.To }} +
+ {{ if eq $transaction.FuncSigStatus 10 }} + {{ $transaction.FuncName }} + {{ else if eq $transaction.FuncSigStatus 1 }} + {{ $transaction.FuncName }} + {{ else }} + {{ $transaction.FuncName }} + {{ end }} + {{ $transaction.Value }} ETH + {{ if gt $transaction.DataLen 0 }} + {{ $transaction.DataLen }} B + + {{ end }} + + {{ if index $inclusionList.TransactionsIncluded $j }} + Yes + {{ else }} + No + {{ end }} + + + {{- "" }}"> +
+
+
+
+ {{ end }} +{{ end }} diff --git a/templates/slot/slot.html b/templates/slot/slot.html index 6a1f76a6..f2d8fe1f 100644 --- a/templates/slot/slot.html +++ b/templates/slot/slot.html @@ -37,6 +37,11 @@

+ {{ if gt .InclusionListsCount 0 }} + + {{ end }} {{ if .Block }} {{ if gt .Block.TransactionsCount 0 }}