diff --git a/clients/consensus/chainspec.go b/clients/consensus/chainspec.go index 8fa135c1..861d39d3 100644 --- a/clients/consensus/chainspec.go +++ b/clients/consensus/chainspec.go @@ -53,6 +53,8 @@ type ChainSpecConfig struct { ElectraForkEpoch *uint64 `yaml:"ELECTRA_FORK_EPOCH" check-if-fork:"ElectraForkEpoch"` FuluForkVersion phase0.Version `yaml:"FULU_FORK_VERSION" check-if-fork:"FuluForkEpoch"` FuluForkEpoch *uint64 `yaml:"FULU_FORK_EPOCH" check-if-fork:"FuluForkEpoch"` + Eip7805ForkVersion phase0.Version `yaml:"EIP7805_FORK_VERSION" check-if-fork:"Eip7805ForkEpoch"` + Eip7805ForkEpoch *uint64 `yaml:"EIP7805_FORK_EPOCH" check-if-fork:"Eip7805ForkEpoch"` // Time parameters SecondsPerSlot uint64 `yaml:"SECONDS_PER_SLOT"` diff --git a/clients/consensus/client.go b/clients/consensus/client.go index b28c8ba7..f5b8fb3c 100644 --- a/clients/consensus/client.go +++ b/clients/consensus/client.go @@ -54,6 +54,7 @@ type Client struct { blockDispatcher utils.Dispatcher[*v1.BlockEvent] headDispatcher utils.Dispatcher[*v1.HeadEvent] checkpointDispatcher utils.Dispatcher[*v1.Finality] + inclusionListDispatcher utils.Dispatcher[*v1.InclusionListEvent] specWarnings []string // warnings from incomplete spec checks specs map[string]interface{} @@ -102,6 +103,10 @@ func (client *Client) SubscribeFinalizedEvent(capacity int) *utils.Subscription[ return client.checkpointDispatcher.Subscribe(capacity, false) } +func (client *Client) SubscribeInclusionListEvent(capacity int, blocking bool) *utils.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 7e5b4a8c..dfed783a 100644 --- a/clients/consensus/clientlogic.go +++ b/clients/consensus/clientlogic.go @@ -133,7 +133,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 @@ -171,6 +171,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()) @@ -367,6 +373,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/go.mod b/go.mod index 165dbeed..5b086190 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,6 @@ require ( require ( github.com/DataDog/zstd v1.5.7 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect @@ -259,3 +258,5 @@ require ( modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.38.2 // indirect ) + +replace github.com/attestantio/go-eth2-client => github.com/pk910/go-eth2-client v0.0.0-20250922213047-288b5f58a08e diff --git a/go.sum b/go.sum index 281561f9..f23abbe8 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,6 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 h1:d/SJkN8/9Ca+1YmuDiUJxAiV4w/a9S8NcsG7GMQSrVI= -github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506/go.mod h1:6TZI4FU6zT8x6ZfWa1J8YQ2NgW0wLV/W3fHRca8ISBo= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= @@ -36,8 +34,6 @@ github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0L github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/attestantio/go-eth2-client v0.28.0 h1:2zIIIMPvSD+g6h3TgVXsoda/Yw3e+wjo1e8CZEanORU= -github.com/attestantio/go-eth2-client v0.28.0/go.mod h1:PO9sHFCq+1RiG+Eh3eOR2GYvYV64Qzg7idM3kLgCs5k= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -568,6 +564,8 @@ github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M= github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4= github.com/pk910/dynamic-ssz v1.1.2 h1:NgovfI4bTKGoHipahOZXtEM8K+UPHW9M5MKEFvhu1sc= github.com/pk910/dynamic-ssz v1.1.2/go.mod h1:HXRWLNcgj3DL65Kznrb+RdL3DEKw2JBZ/6crooqGoII= +github.com/pk910/go-eth2-client v0.0.0-20250922213047-288b5f58a08e h1:4KVrpCeKGViR3S/byg4lGYFRX4HZJEKfSxRoWUTyH/o= +github.com/pk910/go-eth2-client v0.0.0-20250922213047-288b5f58a08e/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= github.com/pk910/hashtree-bindings v0.0.1 h1:Sw+UlPlrBle4LUg04kqLFybVQcfmamwKL1QsrR3GU0g= github.com/pk910/hashtree-bindings v0.0.1/go.mod h1:eayIpxMFkWzMsydESu/5bV8wglZzSE/c9mq6DQdn204= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= diff --git a/handlers/index.go b/handlers/index.go index 60cb29bd..baec02af 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -289,6 +289,14 @@ func buildIndexPageData() (*models.IndexPageData, time.Duration) { ForkDigest: forkDigest[:], }) } + 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, + }) + } // Add BPO forks from BLOB_SCHEDULE elBlobSchedule := services.GlobalBeaconService.GetExecutionChainState().GetFullBlobSchedule() diff --git a/handlers/slot.go b/handlers/slot.go index bc8df344..cb25a333 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 } @@ -769,87 +773,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 - - err := tx.UnmarshalBinary(txBytes) - if err != nil { - logrus.Warnf("error decoding transaction 0x%x.%v: %v\n", pageData.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)) +func getSlotPageInclusionLists(slot phase0.Slot, blockTransactions []*models.SlotPageTransaction) []*models.SlotPageInclusionList { + blockTransactionMap := map[string]bool{} + for _, transaction := range blockTransactions { + blockTransactionMap[string(transaction.Hash)] = true + } - chainId := tx.ChainId() - if chainId != nil && chainId.Cmp(big.NewInt(0)) == 0 { - chainId = nil - } - txFrom, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(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 := []*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)] + } + + 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) { @@ -995,3 +951,86 @@ 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)) + chainId := tx.ChainId() + if chainId != nil && chainId.Cmp(big.NewInt(0)) == 0 { + chainId = nil + } + txFrom, err := ethtypes.Sender(ethtypes.LatestSignerForChainID(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/block_helper.go b/indexer/beacon/block_helper.go index c943ede1..54ef453d 100644 --- a/indexer/beacon/block_helper.go +++ b/indexer/beacon/block_helper.go @@ -47,6 +47,9 @@ func MarshalVersionedSignedBeaconBlockSSZ(dynSsz *dynssz.DynSsz, block *spec.Ver case spec.DataVersionFulu: version = uint64(block.Version) ssz, err = dynSsz.MarshalSSZ(block.Fulu) + case spec.DataVersionEip7805: + version = uint64(block.Version) + ssz, err = dynSsz.MarshalSSZ(block.Eip7805) default: err = fmt.Errorf("unknown block version") } @@ -118,6 +121,11 @@ func UnmarshalVersionedSignedBeaconBlockSSZ(dynSsz *dynssz.DynSsz, version uint6 if err := dynSsz.UnmarshalSSZ(block.Fulu, ssz); err != nil { return nil, fmt.Errorf("failed to decode fulu 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") } @@ -148,6 +156,9 @@ func MarshalVersionedSignedBeaconBlockJson(block *spec.VersionedSignedBeaconBloc case spec.DataVersionFulu: version = uint64(block.Version) jsonRes, err = block.Fulu.MarshalJSON() + case spec.DataVersionEip7805: + version = uint64(block.Version) + jsonRes, err = block.Eip7805.MarshalJSON() default: err = fmt.Errorf("unknown block version") } @@ -201,6 +212,11 @@ func unmarshalVersionedSignedBeaconBlockJson(version uint64, ssz []byte) (*spec. if err := block.Fulu.UnmarshalJSON(ssz); err != nil { return nil, fmt.Errorf("failed to decode fulu 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") } @@ -252,6 +268,12 @@ func getStateRandaoMixes(v *spec.VersionedBeaconState) ([]phase0.Root, error) { } return v.Fulu.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") } @@ -274,6 +296,8 @@ func getStateDepositIndex(state *spec.VersionedBeaconState) uint64 { return state.Electra.ETH1DepositIndex case spec.DataVersionFulu: return state.Fulu.ETH1DepositIndex + case spec.DataVersionEip7805: + return state.EIP7805.ETH1DepositIndex } return 0 } @@ -319,6 +343,12 @@ func getStateCurrentSyncCommittee(v *spec.VersionedBeaconState) ([]phase0.BLSPub } return v.Fulu.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") } @@ -349,6 +379,12 @@ func getStateDepositBalanceToConsume(v *spec.VersionedBeaconState) (phase0.Gwei, } return v.Fulu.DepositBalanceToConsume, nil + case spec.DataVersionEip7805: + if v.EIP7805 == nil { + return 0, errors.New("no eip7805 block") + } + + return v.EIP7805.DepositBalanceToConsume, nil default: return 0, errors.New("unknown version") } @@ -379,6 +415,12 @@ func getStatePendingDeposits(v *spec.VersionedBeaconState) ([]*electra.PendingDe } return v.Fulu.PendingDeposits, nil + case spec.DataVersionEip7805: + if v.EIP7805 == nil || v.EIP7805.PendingDeposits == nil { + return nil, errors.New("no eip7805 block") + } + + return v.EIP7805.PendingDeposits, nil default: return nil, errors.New("unknown version") } @@ -409,6 +451,12 @@ func getStatePendingWithdrawals(v *spec.VersionedBeaconState) ([]*electra.Pendin } return v.Fulu.PendingPartialWithdrawals, nil + case spec.DataVersionEip7805: + if v.EIP7805 == nil || v.EIP7805.PendingPartialWithdrawals == nil { + return nil, errors.New("no eip7805 block") + } + + return v.EIP7805.PendingPartialWithdrawals, nil default: return nil, errors.New("unknown version") } @@ -439,6 +487,12 @@ func getStatePendingConsolidations(v *spec.VersionedBeaconState) ([]*electra.Pen } return v.Fulu.PendingConsolidations, nil + case spec.DataVersionEip7805: + if v.EIP7805 == nil || v.EIP7805.PendingConsolidations == nil { + return nil, errors.New("no eip7805 block") + } + + return v.EIP7805.PendingConsolidations, nil default: return nil, errors.New("unknown version") } @@ -487,6 +541,8 @@ func getBlockSize(dynSsz *dynssz.DynSsz, block *spec.VersionedSignedBeaconBlock) return dynSsz.SizeSSZ(block.Electra) case spec.DataVersionFulu: return dynSsz.SizeSSZ(block.Fulu) + case spec.DataVersionEip7805: + return dynSsz.SizeSSZ(block.Eip7805) default: return 0, errors.New("unknown version") } diff --git a/indexer/beacon/canonical.go b/indexer/beacon/canonical.go index c0d51dde..9d9e6ab6 100644 --- a/indexer/beacon/canonical.go +++ b/indexer/beacon/canonical.go @@ -272,6 +272,8 @@ func (indexer *Indexer) aggregateForkVotes(forkId ForkKey, epochLimit uint64) (t lastSlot := phase0.Slot(0) thisForkId := forkId for { + lastBlocks = lastBlocks[:0] + for _, block := range indexer.blockCache.getLatestBlocks(epochLimit*specs.SlotsPerEpoch, &thisForkId) { lastSlot = block.Slot if block.Slot < minAggregateSlot { diff --git a/indexer/beacon/client.go b/indexer/beacon/client.go index 7f66f1a6..588ecd8b 100644 --- a/indexer/beacon/client.go +++ b/indexer/beacon/client.go @@ -32,8 +32,9 @@ type Client struct { archive bool skipValidators bool - blockSubscription *utils.Subscription[*v1.BlockEvent] - headSubscription *utils.Subscription[*v1.HeadEvent] + blockSubscription *utils.Subscription[*v1.BlockEvent] + headSubscription *utils.Subscription[*v1.HeadEvent] + inclusionListSubscription *utils.Subscription[*v1.InclusionListEvent] headRoot phase0.Root } @@ -81,6 +82,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() } @@ -179,6 +181,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) + } } } @@ -289,6 +296,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, true) diff --git a/indexer/beacon/inclusionlistcache.go b/indexer/beacon/inclusionlistcache.go new file mode 100644 index 00000000..ee2987f2 --- /dev/null +++ b/indexer/beacon/inclusionlistcache.go @@ -0,0 +1,101 @@ +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) + + 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 eca9d0b4..f82139bf 100644 --- a/indexer/beacon/indexer.go +++ b/indexer/beacon/indexer.go @@ -40,12 +40,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 @@ -106,6 +107,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 20cadaa6..5bfcab8a 100644 --- a/indexer/beacon/indexer_getter.go +++ b/indexer/beacon/indexer_getter.go @@ -208,6 +208,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) +} + // GetEpochStatsByEpoch returns the epoch stats for the given epoch. func (indexer *Indexer) GetEpochStatsByEpoch(epoch phase0.Epoch) []*EpochStats { return 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 f14d0f20..4edcfd22 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 }}