Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions pkg/beacon/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ func (d *Default) Healthy(ctx context.Context) (bool, error) {
}

func (d *Default) Peers(ctx context.Context) (types.Peers, error) {
peers := types.Peers{}
peers := make(types.Peers, 0, len(d.nodes))

for _, node := range d.nodes {
status := "connected"
Expand Down Expand Up @@ -749,9 +749,16 @@ func (d *Default) ListFinalizedSlots(ctx context.Context) ([]phase0.Slot, error)
}

latestSlot := phase0.Slot(uint64(finality.Finalized.Epoch) * uint64(sp.SlotsPerEpoch))
slotsPerEpoch := uint64(sp.SlotsPerEpoch)
delta := uint64(0)

for i, val := uint64(latestSlot), uint64(latestSlot)-uint64(sp.SlotsPerEpoch)*uint64(d.config.HistoricalEpochCount); i > val; i -= uint64(sp.SlotsPerEpoch) {
slots = append(slots, phase0.Slot(i))
for count := 0; count < d.config.HistoricalEpochCount; count++ {
if delta > uint64(latestSlot) {
break
}

slots = append(slots, phase0.Slot(uint64(latestSlot)-delta))
delta += slotsPerEpoch
}

return slots, nil
Expand Down
28 changes: 22 additions & 6 deletions pkg/beacon/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
"github.com/sirupsen/logrus"
)

type beaconStateFetcher interface {
FetchBeaconState(ctx context.Context, stateID string) (*spec.VersionedBeaconState, error)
}

func (d *Default) downloadServingCheckpoint(ctx context.Context, checkpoint *v1.Finality) error {
if checkpoint == nil {
return errors.New("checkpoint is nil")
Expand Down Expand Up @@ -172,14 +176,17 @@ func (d *Default) fetchHistoricalCheckpoints(ctx context.Context, checkpoint *v1
// Calculate the epoch boundaries we need to fetch
// We'll derive the current finalized slot and then work back in intervals of SLOTS_PER_EPOCH.
currentSlot := uint64(checkpoint.Finalized.Epoch) * uint64(sp.SlotsPerEpoch)
for i := 1; i < d.config.HistoricalEpochCount; i++ {
if uint64(i)*uint64(sp.SlotsPerEpoch) > currentSlot {
delta := uint64(sp.SlotsPerEpoch)

for offset := 1; offset < d.config.HistoricalEpochCount; offset++ {
if delta > currentSlot {
break
}

slot := phase0.Slot(currentSlot - uint64(i)*uint64(sp.SlotsPerEpoch))
slot := phase0.Slot(currentSlot - delta)

slotsInScope[slot] = struct{}{}
delta += uint64(sp.SlotsPerEpoch)
}

for slot := range slotsInScope {
Expand Down Expand Up @@ -329,7 +336,7 @@ func (d *Default) fetchBundle(ctx context.Context, root phase0.Root, upstream *N

if d.shouldDownloadStates() {
// Download and store beacon state
if err = d.downloadAndStoreBeaconState(ctx, stateRoot, slot, upstream); err != nil {
if err = d.downloadAndStoreBeaconState(ctx, stateRoot, slot, upstream.Beacon); err != nil {
return nil, fmt.Errorf("failed to download and store beacon state: %w", err)
}
}
Expand Down Expand Up @@ -373,14 +380,14 @@ func (d *Default) fetchBundle(ctx context.Context, root phase0.Root, upstream *N
return block, nil
}

func (d *Default) downloadAndStoreBeaconState(ctx context.Context, stateRoot phase0.Root, slot phase0.Slot, node *Node) error {
func (d *Default) downloadAndStoreBeaconState(ctx context.Context, stateRoot phase0.Root, slot phase0.Slot, fetcher beaconStateFetcher) error {
// If the state already exists, don't bother downloading it again.
existingState, err := d.states.GetByStateRoot(stateRoot)
if err == nil && existingState != nil {
return nil
}

beaconState, err := node.Beacon.FetchBeaconState(ctx, eth.SlotAsString(slot))
beaconState, err := fetcher.FetchBeaconState(ctx, eth.RootAsString(stateRoot))
if err != nil {
return fmt.Errorf("failed to fetch beacon state: %w", err)
}
Expand All @@ -389,6 +396,15 @@ func (d *Default) downloadAndStoreBeaconState(ctx context.Context, stateRoot pha
return errors.New("beacon state is nil")
}

computedStateRoot, err := d.sszEncoder.GetStateRoot(beaconState)
if err != nil {
return fmt.Errorf("failed to compute beacon state root: %w", err)
}

if computedStateRoot != stateRoot {
return fmt.Errorf("beacon state root does not match: %#x != %#x", computedStateRoot, stateRoot)
}

expiresAt := time.Now().Add(FinalityHaltedServingPeriod)
if slot == phase0.Slot(0) {
expiresAt = time.Now().Add(999999 * time.Hour)
Expand Down
104 changes: 104 additions & 0 deletions pkg/beacon/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package beacon

import (
"context"
"strings"
"testing"

"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/ethpandaops/checkpointz/pkg/beacon/ssz"
"github.com/ethpandaops/checkpointz/pkg/beacon/store"
"github.com/ethpandaops/checkpointz/pkg/eth"
"github.com/prysmaticlabs/go-bitfield"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/require"
)

type mockBeaconStateFetcher struct {
requestedStateIDs []string
state *spec.VersionedBeaconState
err error
}

func (m *mockBeaconStateFetcher) FetchBeaconState(_ context.Context, stateID string) (*spec.VersionedBeaconState, error) {
m.requestedStateIDs = append(m.requestedStateIDs, stateID)

return m.state, m.err
}

func TestDownloadAndStoreBeaconStateFetchesByRootAndVerifiesState(t *testing.T) {
provider := newTestProvider(t)
state := newTestPhase0BeaconState(phase0.Slot(1))
expectedStateRoot, err := provider.sszEncoder.GetStateRoot(state)
require.NoError(t, err)

fetcher := &mockBeaconStateFetcher{
state: state,
}

err = provider.downloadAndStoreBeaconState(context.Background(), expectedStateRoot, phase0.Slot(1), fetcher)
require.NoError(t, err)
require.Equal(t, []string{eth.RootAsString(expectedStateRoot)}, fetcher.requestedStateIDs)

storedState, err := provider.states.GetByStateRoot(expectedStateRoot)
require.NoError(t, err)
require.Same(t, state, storedState)
}

func TestDownloadAndStoreBeaconStateRejectsMismatchedStateRoot(t *testing.T) {
provider := newTestProvider(t)
state := newTestPhase0BeaconState(phase0.Slot(1))
unexpectedStateRoot, err := provider.sszEncoder.GetStateRoot(newTestPhase0BeaconState(phase0.Slot(2)))
require.NoError(t, err)

fetcher := &mockBeaconStateFetcher{
state: state,
}

err = provider.downloadAndStoreBeaconState(context.Background(), unexpectedStateRoot, phase0.Slot(1), fetcher)
require.ErrorContains(t, err, "beacon state root does not match")
require.Equal(t, []string{eth.RootAsString(unexpectedStateRoot)}, fetcher.requestedStateIDs)

_, err = provider.states.GetByStateRoot(unexpectedStateRoot)
require.Error(t, err)
}

func newTestProvider(t *testing.T) *Default {
t.Helper()

logger, _ := test.NewNullLogger()
namespace := strings.ReplaceAll(t.Name(), "/", "_")

return &Default{
log: logger,
sszEncoder: ssz.NewEncoder(false),
states: store.NewBeaconState(logger, store.Config{MaxItems: 5}, namespace),
}
}

func newTestPhase0BeaconState(slot phase0.Slot) *spec.VersionedBeaconState {
return &spec.VersionedBeaconState{
Version: spec.DataVersionPhase0,
Phase0: &phase0.BeaconState{
Slot: slot,
Fork: &phase0.Fork{},
LatestBlockHeader: &phase0.BeaconBlockHeader{},
BlockRoots: make([]phase0.Root, 8192),
StateRoots: make([]phase0.Root, 8192),
HistoricalRoots: []phase0.Root{},
ETH1Data: &phase0.ETH1Data{BlockHash: make([]byte, 32)},
ETH1DataVotes: []*phase0.ETH1Data{},
Validators: []*phase0.Validator{},
Balances: []phase0.Gwei{},
RANDAOMixes: make([]phase0.Root, 65536),
Slashings: make([]phase0.Gwei, 8192),
PreviousEpochAttestations: []*phase0.PendingAttestation{},
CurrentEpochAttestations: []*phase0.PendingAttestation{},
JustificationBits: bitfield.NewBitvector4(),
PreviousJustifiedCheckpoint: &phase0.Checkpoint{},
CurrentJustifiedCheckpoint: &phase0.Checkpoint{},
FinalizedCheckpoint: &phase0.Checkpoint{},
},
}
}
9 changes: 7 additions & 2 deletions pkg/beacon/expire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import (
"time"

"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/ethpandaops/checkpointz/pkg/eth"
)

var (
defaultSecondsPerSlot = time.Second * 12
)

func CalculateSlotExpiration(slot phase0.Slot, slotsOfHistory int) phase0.Slot {
return slot + phase0.Slot(slotsOfHistory)
if slotsOfHistory < 0 {
return slot
}

return slot + phase0.Slot(uint64(slotsOfHistory))
}

func GetSlotTime(slot phase0.Slot, secondsPerSlot time.Duration, genesis time.Time) time.Time {
return genesis.Add(time.Duration(slot) * secondsPerSlot)
return eth.CalculateSlotTime(slot, genesis, secondsPerSlot).StartTime
}

func TestExpiresAtSlot(t *testing.T) {
Expand Down
19 changes: 18 additions & 1 deletion pkg/eth/slot.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package eth

import (
"math"
"math/big"
"time"

"github.com/attestantio/go-eth2-client/spec/phase0"
Expand All @@ -14,7 +16,22 @@ type SlotTime struct {
}

func CalculateSlotTime(slot phase0.Slot, genesisTime time.Time, durationPerSlot time.Duration) SlotTime {
slotStartTime := genesisTime.Add(time.Duration(slot) * durationPerSlot).UTC()
slotOffset := time.Duration(0)

if durationPerSlot > 0 {
offset := new(big.Int).Mul(
new(big.Int).SetUint64(uint64(slot)),
big.NewInt(int64(durationPerSlot)),
)

if offset.IsInt64() {
slotOffset = time.Duration(offset.Int64())
} else {
slotOffset = time.Duration(math.MaxInt64)
}
}

slotStartTime := genesisTime.Add(slotOffset).UTC()

return SlotTime{
StartTime: slotStartTime,
Expand Down
4 changes: 2 additions & 2 deletions pkg/service/eth/block_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func NewBlockIdentifier(id string) (BlockIdentifier, error) {
return newBlockIdentifier(BlockIDRoot, id), nil
}

if _, err := strconv.ParseInt(id, 10, 64); err == nil {
if _, err := strconv.ParseUint(id, 10, 64); err == nil {
return newBlockIdentifier(BlockIDSlot, id), nil
}

Expand All @@ -83,7 +83,7 @@ func newBlockIdentifier(id BlockIDType, value string) BlockIdentifier {
}

func NewSlotFromString(id string) (phase0.Slot, error) {
slot, err := strconv.ParseInt(id, 10, 64)
slot, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return 0, err
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/service/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package eth
import (
"context"
"fmt"
"strconv"

v1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
Expand Down Expand Up @@ -572,7 +573,12 @@ func (h *Handler) BlobSidecars(ctx context.Context, blockID BlockIdentifier, ind

// Find the sidecar with the given index
for i, sidecar := range sidecars {
if index == int(sidecar.Index) {
sidecarIndex, err := strconv.Atoi(strconv.FormatUint(uint64(sidecar.Index), 10))
if err != nil {
continue
}

if index == sidecarIndex {
filtered = append(filtered, sidecars[i])

break
Expand Down
Loading