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
37 changes: 37 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2840,6 +2840,32 @@ const docTemplate = `{
}
}
},
"api.APIDasGuardianProposerPreference": {
"type": "object",
"properties": {
"dependent_root": {
"type": "string"
},
"fee_recipient": {
"type": "string"
},
"from": {
"type": "string"
},
"gas_limit": {
"type": "integer"
},
"proposal_slot": {
"type": "integer"
},
"received_at": {
"type": "string"
},
"validator_index": {
"type": "integer"
}
}
},
"api.APIDasGuardianScanRequest": {
"type": "object",
"properties": {
Expand All @@ -2860,6 +2886,10 @@ const docTemplate = `{
"items": {
"type": "integer"
}
},
"wait_for_proposer_preferences_seconds": {
"description": "WaitForProposerPreferencesSeconds, if \u003e 0, keeps the Gloas\n` + "`" + `proposer_preferences` + "`" + ` gossip subscription open for this many seconds\nafter the RPC exchange so the gossip mesh has time to deliver messages.\nCapped at 60s server-side. Ignored on pre-Gloas forks.",
"type": "integer"
}
}
},
Expand Down Expand Up @@ -2893,6 +2923,13 @@ const docTemplate = `{
"type": "object",
"additionalProperties": true
},
"proposer_preferences": {
"description": "Gloas SignedProposerPreferences sniffed off the peer's gossip during\nthe scan window. Empty pre-Gloas, or when no proposer published in time.",
"type": "array",
"items": {
"$ref": "#/definitions/api.APIDasGuardianProposerPreference"
}
},
"remote_metadata": {
"description": "Metadata (from RemoteMetadata)",
"allOf": [
Expand Down
37 changes: 37 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2837,6 +2837,32 @@
}
}
},
"api.APIDasGuardianProposerPreference": {
"type": "object",
"properties": {
"dependent_root": {
"type": "string"
},
"fee_recipient": {
"type": "string"
},
"from": {
"type": "string"
},
"gas_limit": {
"type": "integer"
},
"proposal_slot": {
"type": "integer"
},
"received_at": {
"type": "string"
},
"validator_index": {
"type": "integer"
}
}
},
"api.APIDasGuardianScanRequest": {
"type": "object",
"properties": {
Expand All @@ -2857,6 +2883,10 @@
"items": {
"type": "integer"
}
},
"wait_for_proposer_preferences_seconds": {
"description": "WaitForProposerPreferencesSeconds, if \u003e 0, keeps the Gloas\n`proposer_preferences` gossip subscription open for this many seconds\nafter the RPC exchange so the gossip mesh has time to deliver messages.\nCapped at 60s server-side. Ignored on pre-Gloas forks.",
"type": "integer"
}
}
},
Expand Down Expand Up @@ -2890,6 +2920,13 @@
"type": "object",
"additionalProperties": true
},
"proposer_preferences": {
"description": "Gloas SignedProposerPreferences sniffed off the peer's gossip during\nthe scan window. Empty pre-Gloas, or when no proposer published in time.",
"type": "array",
"items": {
"$ref": "#/definitions/api.APIDasGuardianProposerPreference"
}
},
"remote_metadata": {
"description": "Metadata (from RemoteMetadata)",
"allOf": [
Expand Down
31 changes: 31 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,23 @@ definitions:
syncnets:
type: string
type: object
api.APIDasGuardianProposerPreference:
properties:
dependent_root:
type: string
fee_recipient:
type: string
from:
type: string
gas_limit:
type: integer
proposal_slot:
type: integer
received_at:
type: string
validator_index:
type: integer
type: object
api.APIDasGuardianScanRequest:
properties:
enr:
Expand All @@ -307,6 +324,13 @@ definitions:
items:
type: integer
type: array
wait_for_proposer_preferences_seconds:
description: |-
WaitForProposerPreferencesSeconds, if > 0, keeps the Gloas
`proposer_preferences` gossip subscription open for this many seconds
after the RPC exchange so the gossip mesh has time to deliver messages.
Capped at 60s server-side. Ignored on pre-Gloas forks.
type: integer
type: object
api.APIDasGuardianScanResponse:
properties:
Expand All @@ -327,6 +351,13 @@ definitions:
additionalProperties: true
description: P2P Information
type: object
proposer_preferences:
description: |-
Gloas SignedProposerPreferences sniffed off the peer's gossip during
the scan window. Empty pre-Gloas, or when no proposer published in time.
items:
$ref: '#/definitions/api.APIDasGuardianProposerPreference'
type: array
remote_metadata:
allOf:
- $ref: '#/definitions/api.APIDasGuardianMetadata'
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/allegro/bigcache/v3 v3.1.0
github.com/cockroachdb/pebble v1.1.5
github.com/ethereum/go-ethereum v1.17.2
github.com/ethpandaops/eth-das-guardian v0.1.1
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522095023-d09ab17a2b35
github.com/ethpandaops/ethcore v0.0.0-20260320045412-9cdd5d70a29c
github.com/ethpandaops/ethwallclock v0.4.0
github.com/ethpandaops/go-eth2-client v0.1.2
Expand Down
12 changes: 10 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,16 @@ github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJ
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI=
github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o=
github.com/ethpandaops/eth-das-guardian v0.1.1 h1:RN96h3HSAMyU4XUOCqkj/9+lB+UW7cVW4Wr4JzdjmZY=
github.com/ethpandaops/eth-das-guardian v0.1.1/go.mod h1:7amdK4bN9N9Zp0b9Y9FcbDm1YrpeGBm8ix8qpiX0WuY=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522080519-13921e4ca7f1 h1:QbgdX6WLkOSGA6YFu3pg235nvNPgyHWzAORZhqf5WzE=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522080519-13921e4ca7f1/go.mod h1:nNlQGSz4cf8FPBuf0KIXHt1/lPfsuKXl1VfLipaUPeE=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522082151-67ee99d84f56 h1:UtfnQdPA8W5X6RiUOaeNisnjw/BZpcVFkeCdja08lFI=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522082151-67ee99d84f56/go.mod h1:nNlQGSz4cf8FPBuf0KIXHt1/lPfsuKXl1VfLipaUPeE=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522090705-b0aedc982964 h1:BRAxqF/Tor8PApcnFsevGzT7OfU933sNScThLI0TDw0=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522090705-b0aedc982964/go.mod h1:nNlQGSz4cf8FPBuf0KIXHt1/lPfsuKXl1VfLipaUPeE=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522091055-3f6edd4ccde3 h1:K8h1AX/mBg/s3EQTyrDH4yJsOx1/BfXo6DJDNH5zFtM=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522091055-3f6edd4ccde3/go.mod h1:nNlQGSz4cf8FPBuf0KIXHt1/lPfsuKXl1VfLipaUPeE=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522095023-d09ab17a2b35 h1:uJR6xuKYt8OMIsb+qng0XPE65VSXOn/Kn2dRQbVsm4U=
github.com/ethpandaops/eth-das-guardian v0.1.2-0.20260522095023-d09ab17a2b35/go.mod h1:nNlQGSz4cf8FPBuf0KIXHt1/lPfsuKXl1VfLipaUPeE=
github.com/ethpandaops/ethcore v0.0.0-20260320045412-9cdd5d70a29c h1:uBRIitwcuCJlRGioqm0jQRIojiH8DSyLRFSTCCBxN6o=
github.com/ethpandaops/ethcore v0.0.0-20260320045412-9cdd5d70a29c/go.mod h1:QsmYTdesob+vQ6pW4KtRVvxLZUNop3cdtd/DgD30hJU=
github.com/ethpandaops/ethwallclock v0.4.0 h1:+sgnhf4pk6hLPukP076VxkiLloE4L0Yk1yat+ZyHh1g=
Expand Down
67 changes: 65 additions & 2 deletions handlers/api/api_das_guardian.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ type APIDasGuardianScanRequest struct {
Slots []uint64 `json:"slots,omitempty"` // Optional slot numbers to scan
RandomMode string `json:"random_mode,omitempty"` // Random slot selection mode: "non_missed", "with_blobs", "available"
RandomCount int32 `json:"random_count,omitempty"` // Number of random slots to select (default: 4)
// WaitForProposerPreferencesSeconds, if > 0, keeps the Gloas
// `proposer_preferences` gossip subscription open for this many seconds
// after the RPC exchange so the gossip mesh has time to deliver messages.
// Capped at 60s server-side. Ignored on pre-Gloas forks.
WaitForProposerPreferencesSeconds int32 `json:"wait_for_proposer_preferences_seconds,omitempty"`
}

// APIDasGuardianScanResponse represents the response from DAS Guardian scan
Expand All @@ -42,10 +47,26 @@ type APIDasGuardianScanResult struct {
// Metadata (from RemoteMetadata)
RemoteMetadata *APIDasGuardianMetadata `json:"remote_metadata,omitempty"`

// Gloas SignedProposerPreferences sniffed off the peer's gossip during
// the scan window. Empty pre-Gloas, or when no proposer published in time.
ProposerPreferences []*APIDasGuardianProposerPreference `json:"proposer_preferences,omitempty"`

// DAS Evaluation Result
EvalResult *APIDasGuardianEvalResult `json:"eval_result,omitempty"`
}

// APIDasGuardianProposerPreference is one SignedProposerPreferences gossip
// message observed during the scan, attributed to the peer that delivered it.
type APIDasGuardianProposerPreference struct {
ValidatorIndex uint64 `json:"validator_index"`
ProposalSlot uint64 `json:"proposal_slot"`
FeeRecipient string `json:"fee_recipient"`
GasLimit uint64 `json:"gas_limit"`
DependentRoot string `json:"dependent_root"`
ReceivedAt string `json:"received_at"`
From string `json:"from"`
}

// APIDasGuardianStatus represents the beacon node status
type APIDasGuardianStatus struct {
ForkDigest string `json:"fork_digest"`
Expand Down Expand Up @@ -110,11 +131,31 @@ func APIDasGuardianScan(w http.ResponseWriter, r *http.Request) {
return
}

// Honour an optional wait window so the Gloas proposer_preferences
// gossip subscription has time to deliver messages. Cap at 60s so a
// caller can't tie up a worker indefinitely.
waitSeconds := req.WaitForProposerPreferencesSeconds
if waitSeconds < 0 {
waitSeconds = 0
}
if waitSeconds > 60 {
waitSeconds = 60
}
scanTimeout := 30 * time.Second
if extra := time.Duration(waitSeconds) * time.Second; extra > 0 {
scanTimeout = 30*time.Second + extra
}

// Create temporary DAS Guardian instance
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
ctx, cancel := context.WithTimeout(r.Context(), scanTimeout)
defer cancel()

dasGuardian, err := services.NewDasGuardian(ctx, logrus.WithField("component", "das-guardian-api"))
guardianOpts := make([]services.DasGuardianOption, 0, 1)
if waitSeconds > 0 {
guardianOpts = append(guardianOpts, services.WithProposerPreferencesWait(time.Duration(waitSeconds)*time.Second))
}

dasGuardian, err := services.NewDasGuardian(ctx, logrus.WithField("component", "das-guardian-api"), guardianOpts...)
if err != nil {
logrus.WithError(err).Error("failed to create DAS Guardian instance")
http.Error(w, `{"error": "failed to initialize DAS Guardian"}`, http.StatusInternalServerError)
Expand Down Expand Up @@ -199,6 +240,28 @@ func APIDasGuardianScan(w http.ResponseWriter, r *http.Request) {
}
}

// Map any Gloas SignedProposerPreferences messages that were observed
// off the wire during the scan window.
if len(scanResult.ProposerPreferences) > 0 {
prefs := make([]*APIDasGuardianProposerPreference, 0, len(scanResult.ProposerPreferences))
for _, obs := range scanResult.ProposerPreferences {
if obs == nil || obs.Message == nil || obs.Message.Message == nil {
continue
}
msg := obs.Message.Message
prefs = append(prefs, &APIDasGuardianProposerPreference{
ValidatorIndex: uint64(msg.ValidatorIndex),
ProposalSlot: uint64(msg.ProposalSlot),
FeeRecipient: fmt.Sprintf("0x%x", msg.FeeRecipient),
GasLimit: msg.GasLimit,
DependentRoot: fmt.Sprintf("0x%x", msg.DependentRoot),
ReceivedAt: obs.ReceivedAt.Format(time.RFC3339),
From: obs.From.String(),
})
}
result.ProposerPreferences = prefs
}

// Map evaluation result (always present since it's not a pointer)
// Extract data from RangeResult and RootResult arrays
var downloadedResult [][]string
Expand Down
19 changes: 18 additions & 1 deletion services/dasguardian.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type DasGuardian struct {
guardian *dasguardian.DasGuardian
}

func NewDasGuardian(ctx context.Context, logger logrus.FieldLogger) (*DasGuardian, error) {
func NewDasGuardian(ctx context.Context, logger logrus.FieldLogger, options ...DasGuardianOption) (*DasGuardian, error) {
guardianApi := &dasGuardianAPI{}

// Convert FieldLogger to *logrus.Logger
Expand All @@ -39,6 +39,9 @@ func NewDasGuardian(ctx context.Context, logger logrus.FieldLogger) (*DasGuardia
ConnectionTimeout: 10 * time.Second,
InitTimeout: 1 * time.Second,
}
for _, opt := range options {
opt(opts)
}

guardian, err := dasguardian.NewDASGuardian(ctx, opts)
if err != nil {
Expand All @@ -50,6 +53,20 @@ func NewDasGuardian(ctx context.Context, logger logrus.FieldLogger) (*DasGuardia
}, nil
}

// DasGuardianOption customises the DasGuardianConfig used to construct the
// underlying guardian. Used to enable Gloas-only paths like the proposer
// preferences gossip wait without affecting the default scan.
type DasGuardianOption func(*dasguardian.DasGuardianConfig)

// WithProposerPreferencesWait configures the underlying scan to keep the
// `proposer_preferences` gossip subscription open for the given duration so
// the gossip mesh has time to deliver messages.
func WithProposerPreferencesWait(d time.Duration) DasGuardianOption {
return func(cfg *dasguardian.DasGuardianConfig) {
cfg.ProposerPreferencesWaitDuration = d
}
}

func (d *DasGuardian) Close() error {
return d.guardian.Close()
}
Expand Down
Loading