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
59 changes: 38 additions & 21 deletions app/eth2wrap/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,8 @@ var (
// BasisPoints is the total number of basis points, ie. 100% of the slot duration.
const BasisPoints = 10_000

// gloasSuffix is appended to the intra-slot deadline spec keys that the gloas fork overrides.
const gloasSuffix = "_GLOAS"

// ForkBPS defines an intra-slot duty deadline in basis points of the slot duration,
// both before and after the gloas fork.
// ForkBPS defines an intra-slot duty deadline in basis points of the slot duration, per fork.
// PreGloas is zero for deadlines that the gloas fork introduces, since they don't apply before it.
type ForkBPS struct {
PreGloas uint64
Gloas uint64
Expand All @@ -72,6 +69,11 @@ type SlotTimingConfig struct {
Aggregate ForkBPS
SyncMessage ForkBPS
Contribution ForkBPS
// Payload is the deadline for the builder to reveal the execution payload.
Payload ForkBPS
// PayloadAttestation is the deadline for payload timeliness committee members to broadcast
// payload attestations.
PayloadAttestation ForkBPS
// GloasEpoch is the epoch at which the gloas deadlines take effect. It is math.MaxUint64
// if the beacon node doesn't publish GLOAS_FORK_EPOCH or hasn't scheduled the fork.
GloasEpoch eth2p0.Epoch
Expand All @@ -80,10 +82,12 @@ type SlotTimingConfig struct {
// Intra-slot duty deadlines as basis points of the slot duration as defined by the consensus spec.
// These are applied for beacon nodes that predate the corresponding spec keys.
var (
defaultAttestationBPS = ForkBPS{PreGloas: 3333, Gloas: 2500}
defaultAggregateBPS = ForkBPS{PreGloas: 6667, Gloas: 5000}
defaultSyncMessageBPS = ForkBPS{PreGloas: 3333, Gloas: 2500}
defaultContributionBPS = ForkBPS{PreGloas: 6667, Gloas: 5000}
defaultAttestationBPS = ForkBPS{PreGloas: 3333, Gloas: 2500}
defaultAggregateBPS = ForkBPS{PreGloas: 6667, Gloas: 5000}
defaultSyncMessageBPS = ForkBPS{PreGloas: 3333, Gloas: 2500}
defaultContributionBPS = ForkBPS{PreGloas: 6667, Gloas: 5000}
defaultPayloadBPS = ForkBPS{Gloas: 5000}
defaultPayloadAttestationBPS = ForkBPS{Gloas: 7500}
)

// FetchSlotTimingConfig returns the network's intra-slot duty deadlines.
Expand All @@ -109,27 +113,40 @@ func parseSlotTimingConfig(data map[string]any) (SlotTimingConfig, error) {
resp.GloasEpoch = eth2p0.Epoch(epoch)
}

// Note that the deadlines introduced by the gloas fork have no pre-gloas key, since the
// unsuffixed key is itself the gloas value.
for _, field := range []struct {
Key string
Default ForkBPS
Resolved *ForkBPS
PreGloasKey string
GloasKey string
Default ForkBPS
Resolved *ForkBPS
}{
{Key: "ATTESTATION_DUE_BPS", Default: defaultAttestationBPS, Resolved: &resp.Attestation},
{Key: "AGGREGATE_DUE_BPS", Default: defaultAggregateBPS, Resolved: &resp.Aggregate},
{Key: "SYNC_MESSAGE_DUE_BPS", Default: defaultSyncMessageBPS, Resolved: &resp.SyncMessage},
{Key: "CONTRIBUTION_DUE_BPS", Default: defaultContributionBPS, Resolved: &resp.Contribution},
{PreGloasKey: "ATTESTATION_DUE_BPS", GloasKey: "ATTESTATION_DUE_BPS_GLOAS", Default: defaultAttestationBPS, Resolved: &resp.Attestation},
{PreGloasKey: "AGGREGATE_DUE_BPS", GloasKey: "AGGREGATE_DUE_BPS_GLOAS", Default: defaultAggregateBPS, Resolved: &resp.Aggregate},
{PreGloasKey: "SYNC_MESSAGE_DUE_BPS", GloasKey: "SYNC_MESSAGE_DUE_BPS_GLOAS", Default: defaultSyncMessageBPS, Resolved: &resp.SyncMessage},
{PreGloasKey: "CONTRIBUTION_DUE_BPS", GloasKey: "CONTRIBUTION_DUE_BPS_GLOAS", Default: defaultContributionBPS, Resolved: &resp.Contribution},
{GloasKey: "PAYLOAD_DUE_BPS", Default: defaultPayloadBPS, Resolved: &resp.Payload},
{GloasKey: "PAYLOAD_ATTESTATION_DUE_BPS", Default: defaultPayloadAttestationBPS, Resolved: &resp.PayloadAttestation},
} {
preGloas, err := parseBPS(data, field.Key, field.Default.PreGloas)
if err != nil {
return SlotTimingConfig{}, err
resolved := field.Default

if field.PreGloasKey != "" {
preGloas, err := parseBPS(data, field.PreGloasKey, field.Default.PreGloas)
if err != nil {
return SlotTimingConfig{}, err
}

resolved.PreGloas = preGloas
}

gloas, err := parseBPS(data, field.Key+gloasSuffix, field.Default.Gloas)
gloas, err := parseBPS(data, field.GloasKey, field.Default.Gloas)
if err != nil {
return SlotTimingConfig{}, err
}

*field.Resolved = ForkBPS{PreGloas: preGloas, Gloas: gloas}
resolved.Gloas = gloas

*field.Resolved = resolved
}

return resp, nil
Expand Down
61 changes: 45 additions & 16 deletions app/eth2wrap/utils_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ func TestFetchNetworkSpecErrors(t *testing.T) {
func TestParseSlotTimingConfig(t *testing.T) {
// specDefaults is the config returned when the beacon node publishes none of the keys.
specDefaults := SlotTimingConfig{
Attestation: ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
GloasEpoch: math.MaxUint64,
Attestation: ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
Payload: ForkBPS{Gloas: 5000},
PayloadAttestation: ForkBPS{Gloas: 7500},
GloasEpoch: math.MaxUint64,
}

tests := []struct {
Expand All @@ -93,16 +95,41 @@ func TestParseSlotTimingConfig(t *testing.T) {
"CONTRIBUTION_DUE_BPS": uint64(6667),
"CONTRIBUTION_DUE_BPS_GLOAS": uint64(5000),
"GLOAS_FORK_EPOCH": uint64(1024),
"PAYLOAD_ATTESTATION_DUE_BPS": uint64(7500), // Unused, must be ignored.
"PAYLOAD_DUE_BPS": uint64(5000),
"PAYLOAD_ATTESTATION_DUE_BPS": uint64(7500),
"INCLUSION_LIST_DUE_BPS": uint64(6667), // Heze only, must be ignored.
},
expect: SlotTimingConfig{
Attestation: ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
GloasEpoch: 1024,
Attestation: ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
Payload: ForkBPS{Gloas: 5000},
PayloadAttestation: ForkBPS{Gloas: 7500},
GloasEpoch: 1024,
},
},
{
name: "custom payload deadlines",
data: map[string]any{
"PAYLOAD_DUE_BPS": uint64(4000),
"PAYLOAD_ATTESTATION_DUE_BPS": uint64(8000),
},
expect: SlotTimingConfig{
Attestation: ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
Payload: ForkBPS{Gloas: 4000},
PayloadAttestation: ForkBPS{Gloas: 8000},
GloasEpoch: math.MaxUint64,
},
},
{
name: "invalid payload attestation basis points",
data: map[string]any{"PAYLOAD_ATTESTATION_DUE_BPS": uint64(10001)},
errorIs: "invalid basis points in network spec",
},
{
name: "custom values override defaults",
data: map[string]any{
Expand All @@ -111,11 +138,13 @@ func TestParseSlotTimingConfig(t *testing.T) {
"GLOAS_FORK_EPOCH": uint64(0),
},
expect: SlotTimingConfig{
Attestation: ForkBPS{PreGloas: 2000, Gloas: 1500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
GloasEpoch: 0,
Attestation: ForkBPS{PreGloas: 2000, Gloas: 1500},
Aggregate: ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: ForkBPS{PreGloas: 6667, Gloas: 5000},
Payload: ForkBPS{Gloas: 5000},
PayloadAttestation: ForkBPS{Gloas: 7500},
GloasEpoch: 0,
},
},
{
Expand Down
10 changes: 6 additions & 4 deletions app/eth2wrap/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ func TestFetchSlotTimingConfig(t *testing.T) {
require.Equal(t, eth2wrap.SlotTimingConfig{
Attestation: eth2wrap.ForkBPS{PreGloas: 2000, Gloas: 1500},
// Keys the beacon node doesn't publish default to the consensus spec values.
Aggregate: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: eth2wrap.ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
GloasEpoch: 1024,
Aggregate: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: eth2wrap.ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
Payload: eth2wrap.ForkBPS{Gloas: 5000},
PayloadAttestation: eth2wrap.ForkBPS{Gloas: 7500},
GloasEpoch: 1024,
}, timing)
}

Expand Down
9 changes: 5 additions & 4 deletions core/slotoffset.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ func NewSlotOffsetFunc(ctx context.Context, eth2Cl eth2wrap.Client) (SlotOffsetF
// newSlotOffsetFunc returns a slot offset function for the provided spec values.
func newSlotOffsetFunc(slotDuration time.Duration, slotsPerEpoch uint64, timing eth2wrap.SlotTimingConfig) SlotOffsetFunc {
bpsByDuty := map[DutyType]eth2wrap.ForkBPS{
DutyAttester: timing.Attestation,
DutyAggregator: timing.Aggregate,
DutySyncMessage: timing.SyncMessage,
DutySyncContribution: timing.Contribution,
DutyAttester: timing.Attestation,
DutyAggregator: timing.Aggregate,
DutySyncMessage: timing.SyncMessage,
DutySyncContribution: timing.Contribution,
DutyPayloadAttestation: timing.Payload,
}

gloasSlot, gloasScheduled := forkSlot(timing.GloasEpoch, slotsPerEpoch)
Expand Down
26 changes: 21 additions & 5 deletions core/slotoffset_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,30 @@ import (
// mainnetTiming returns the intra-slot deadlines of a network that schedules the gloas fork at epoch 64.
func mainnetTiming() eth2wrap.SlotTimingConfig {
return eth2wrap.SlotTimingConfig{
Attestation: eth2wrap.ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: eth2wrap.ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
GloasEpoch: 64,
Attestation: eth2wrap.ForkBPS{PreGloas: 3333, Gloas: 2500},
Aggregate: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
SyncMessage: eth2wrap.ForkBPS{PreGloas: 3333, Gloas: 2500},
Contribution: eth2wrap.ForkBPS{PreGloas: 6667, Gloas: 5000},
Payload: eth2wrap.ForkBPS{Gloas: 5000},
PayloadAttestation: eth2wrap.ForkBPS{Gloas: 7500},
GloasEpoch: 64,
}
}

func TestSlotOffsetPayloadAttestation(t *testing.T) {
offsetFunc := newSlotOffsetFunc(12*time.Second, 16, mainnetTiming())

const gloasSlot = 64 * 16

// The payload attestation duty is triggered when the builder's payload is due (1/2 into the
// slot), not at its own deadline of 3/4 into the slot, so that consensus, partial signature
// exchange and submission can complete before the deadline.
require.Equal(t, 6*time.Second, offsetFunc(Duty{Slot: gloasSlot, Type: DutyPayloadAttestation}))

// The duty doesn't exist before the gloas fork.
require.Zero(t, offsetFunc(Duty{Slot: gloasSlot - 1, Type: DutyPayloadAttestation}))
}

func TestSlotOffsetPreGloasMatchesFractions(t *testing.T) {
// A 12 second slot duration must resolve to the exact fractions used before the gloas fork,
// since 3333 and 6667 basis points are the consensus spec's approximations of 1/3 and 2/3.
Expand Down
13 changes: 12 additions & 1 deletion core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ const (
DutyPrepareSyncContribution DutyType = 11
DutySyncContribution DutyType = 12
DutyInfoSync DutyType = 13
DutyPayloadAttestation DutyType = 14
// Only ever append new types here...

dutySentinel DutyType = 14 // Must always be last
dutySentinel DutyType = 15 // Must always be last
)

func (d DutyType) Valid() bool {
Expand All @@ -69,6 +70,7 @@ func (d DutyType) String() string {
DutyPrepareSyncContribution: "prepare_sync_contribution",
DutySyncContribution: "sync_contribution",
DutyInfoSync: "info_sync",
DutyPayloadAttestation: "payload_attestation",
}[d]
}

Expand Down Expand Up @@ -260,6 +262,15 @@ func NewInfoSyncDuty(slot uint64) Duty {
}
}

// NewPayloadAttestationDuty returns a new payload attestation duty. It is a convenience function
// that is slightly more readable and concise than the struct literal equivalent.
func NewPayloadAttestationDuty(slot uint64) Duty {
return Duty{
Slot: slot,
Type: DutyPayloadAttestation,
}
}

const (
pkLen = 98 // "0x" + hex.Encode([48]byte) = 2+2*48
sigLen = 96
Expand Down
13 changes: 11 additions & 2 deletions core/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ func TestBackwardsCompatibility(t *testing.T) {
require.EqualValues(t, 11, core.DutyPrepareSyncContribution)
require.EqualValues(t, 12, core.DutySyncContribution)
require.EqualValues(t, 13, core.DutyInfoSync)
require.EqualValues(t, 14, core.DutyPayloadAttestation)
// Add more types here.

const sentinel = core.DutyType(14)
const sentinel = core.DutyType(15)
for i := core.DutyUnknown; i <= sentinel; i++ {
switch i {
case core.DutyUnknown:
Expand Down Expand Up @@ -78,7 +79,7 @@ func TestWithDutySpanCtx(t *testing.T) {
func TestAllDutyTypes(t *testing.T) {
adt := core.AllDutyTypes()

require.Len(t, adt, 13)
require.Len(t, adt, 14)

for i, dt := range adt {
require.Equal(t, i, slices.Index(adt, dt))
Expand Down Expand Up @@ -149,6 +150,14 @@ func TestNewInfoSyncDuty(t *testing.T) {
require.EqualValues(t, 1, d.Slot)
}

func TestNewPayloadAttestationDuty(t *testing.T) {
d := core.NewPayloadAttestationDuty(1)

require.Equal(t, core.DutyPayloadAttestation, d.Type)
require.Equal(t, "1/payload_attestation", d.String())
require.EqualValues(t, 1, d.Slot)
}

func TestPubKeyFrom48Bytes(t *testing.T) {
k := testutil.RandomEth2PubKey(t)
pk := core.PubKeyFrom48Bytes(k)
Expand Down
1 change: 1 addition & 0 deletions eth2util/signing/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
DomainContributionAndProof DomainName = "DOMAIN_CONTRIBUTION_AND_PROOF"
DomainDeposit DomainName = "DOMAIN_DEPOSIT"
DomainBlobSidecar DomainName = "DOMAIN_BLOB_SIDECAR"
DomainPTCAttester DomainName = "DOMAIN_PTC_ATTESTER"
)

// GetDomain returns the beacon domain for the provided type.
Expand Down
13 changes: 13 additions & 0 deletions eth2util/signing/signing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ import (
"github.com/obolnetwork/charon/testutil/beaconmock"
)

func TestGetDomainPTCAttester(t *testing.T) {
// The gloas fork adds DOMAIN_PTC_ATTESTER for payload timeliness attestations.
// See https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.13/specs/gloas/beacon-chain.md
bmock, err := beaconmock.New(t.Context(), beaconmock.WithSpecOverride("DOMAIN_PTC_ATTESTER", "0x0c000000"))
require.NoError(t, err)

domain, err := signing.GetDomain(t.Context(), bmock, signing.DomainPTCAttester, 0)
require.NoError(t, err)

// The first four bytes of a signing domain are its domain type.
require.Equal(t, []byte{0x0c, 0x00, 0x00, 0x00}, domain[:4])
}

func TestVerifyRegistrationReference(t *testing.T) {
bmock, err := beaconmock.New(t.Context())
require.NoError(t, err)
Expand Down
Loading