Skip to content

Commit 094953a

Browse files
authored
*: check fee recipient on local (#4474)
* Fix warning for mismatching fee recipient to be on blinded block rather than PBS enabled * Add metrics and healthchecks * Fix metrics.md
1 parent 35df7a4 commit 094953a

File tree

6 files changed

+64
-66
lines changed

6 files changed

+64
-66
lines changed

app/health/checks.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,26 @@ var checks = []check{
261261
return maxVal == 2, nil // 1=blinded (MEV), 2=local block
262262
},
263263
},
264+
{
265+
Name: "local_proposal_fee_recipient_mismatch",
266+
Description: `Local block proposal has a mismatched fee recipient.
267+
The fee recipient in the fetched local proposal does not match the expected fee recipient configured for the validator.
268+
This means block rewards are highly likely to be sent to the wrong address.
269+
270+
This usually happens in a rare scenario where Charon has multiple BNs set and a subset of them fail the prepare proposer call (https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/prepareBeaconProposer),
271+
but another subsest succeed. Then when the proposal is fetched, it is fetched from the BN that failed the prepare proposer call, which returns a local block with an unexpected fee recipient (likely the zero burn address).
272+
273+
At this stage there is not straightforward fix for that. What an operator can do is to identify the faulty BN (by checking which one has failed prepare proposer calls in its logs) and remove it from the subset of BNs.`,
274+
Severity: severityCritical,
275+
Func: func(q query, _ Metadata) (bool, error) {
276+
maxVal, err := q("core_fetcher_proposal_local_mismatch_fee_recipient", noLabels, gaugeMax)
277+
if err != nil {
278+
return false, err
279+
}
280+
281+
return maxVal == 2.0, nil // 0.0=N/A, 1.0=match, 2.0=mismatch
282+
},
283+
},
264284
{
265285
Name: "high_beacon_node_sse_head_delay",
266286
Description: `Beacon node SSE head received after 4s for >4% of the blocks in the past hour.

app/health/checks_internal_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,28 @@ func TestLocalBlockProposalCheck(t *testing.T) {
457457
})
458458
}
459459

460+
func TestLocalProposalFeeRecipientMismatchCheck(t *testing.T) {
461+
m := Metadata{}
462+
checkName := "local_proposal_fee_recipient_mismatch"
463+
metricName := "core_fetcher_proposal_local_mismatch_fee_recipient"
464+
465+
t.Run("no data", func(t *testing.T) {
466+
testCheck(t, m, checkName, false, nil)
467+
})
468+
469+
t.Run("fee recipient matches", func(t *testing.T) {
470+
testCheck(t, m, checkName, false,
471+
genFam(metricName, genGauge(nil, 1, 1, 1)),
472+
)
473+
})
474+
475+
t.Run("fee recipient mismatch", func(t *testing.T) {
476+
testCheck(t, m, checkName, true,
477+
genFam(metricName, genGauge(nil, 2, 2, 2)),
478+
)
479+
})
480+
}
481+
460482
func TestHighParsigdbStoreLatencyCheck(t *testing.T) {
461483
m := Metadata{}
462484
checkName := "high_parsigdb_store_latency"

core/fetcher/fetcher.go

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ func (f *Fetcher) fetchProposerData(ctx context.Context, slot uint64, defSet cor
353353
proposal := eth2Resp.Data
354354

355355
// Builders set fee recipient to themselves so it's always different from validator's.
356-
if !f.builderEnabled {
356+
if !proposal.Blinded {
357357
// Ensure fee recipient is correctly populated in proposal.
358358
verifyFeeRecipient(ctx, proposal, f.feeRecipientFunc(pubkey))
359359
}
@@ -456,42 +456,25 @@ func verifyFeeRecipient(ctx context.Context, proposal *eth2api.VersionedProposal
456456

457457
switch proposal.Version {
458458
case eth2spec.DataVersionBellatrix:
459-
if proposal.Blinded {
460-
actualAddr = fmt.Sprintf("%#x", proposal.BellatrixBlinded.Body.ExecutionPayloadHeader.FeeRecipient)
461-
} else {
462-
actualAddr = fmt.Sprintf("%#x", proposal.Bellatrix.Body.ExecutionPayload.FeeRecipient)
463-
}
459+
actualAddr = fmt.Sprintf("%#x", proposal.Bellatrix.Body.ExecutionPayload.FeeRecipient)
464460
case eth2spec.DataVersionCapella:
465-
if proposal.Blinded {
466-
actualAddr = fmt.Sprintf("%#x", proposal.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient)
467-
} else {
468-
actualAddr = fmt.Sprintf("%#x", proposal.Capella.Body.ExecutionPayload.FeeRecipient)
469-
}
461+
actualAddr = fmt.Sprintf("%#x", proposal.Capella.Body.ExecutionPayload.FeeRecipient)
470462
case eth2spec.DataVersionDeneb:
471-
if proposal.Blinded {
472-
actualAddr = fmt.Sprintf("%#x", proposal.DenebBlinded.Body.ExecutionPayloadHeader.FeeRecipient)
473-
} else {
474-
actualAddr = fmt.Sprintf("%#x", proposal.Deneb.Block.Body.ExecutionPayload.FeeRecipient)
475-
}
463+
actualAddr = fmt.Sprintf("%#x", proposal.Deneb.Block.Body.ExecutionPayload.FeeRecipient)
476464
case eth2spec.DataVersionElectra:
477-
if proposal.Blinded {
478-
actualAddr = fmt.Sprintf("%#x", proposal.ElectraBlinded.Body.ExecutionPayloadHeader.FeeRecipient)
479-
} else {
480-
actualAddr = fmt.Sprintf("%#x", proposal.Electra.Block.Body.ExecutionPayload.FeeRecipient)
481-
}
465+
actualAddr = fmt.Sprintf("%#x", proposal.Electra.Block.Body.ExecutionPayload.FeeRecipient)
482466
case eth2spec.DataVersionFulu:
483-
if proposal.Blinded {
484-
actualAddr = fmt.Sprintf("%#x", proposal.FuluBlinded.Body.ExecutionPayloadHeader.FeeRecipient)
485-
} else {
486-
actualAddr = fmt.Sprintf("%#x", proposal.Fulu.Block.Body.ExecutionPayload.FeeRecipient)
487-
}
467+
actualAddr = fmt.Sprintf("%#x", proposal.Fulu.Block.Body.ExecutionPayload.FeeRecipient)
488468
default:
489469
return
490470
}
491471

492472
if actualAddr != "" && !strings.EqualFold(actualAddr, feeRecipientAddress) {
493473
log.Warn(ctx, "Proposal with unexpected fee recipient address", nil,
494474
z.Str("expected", feeRecipientAddress), z.Str("actual", actualAddr))
475+
proposalLocalMismatchFeeRecipientGauge.Set(2.0)
476+
} else {
477+
proposalLocalMismatchFeeRecipientGauge.Set(1.0)
495478
}
496479
}
497480

core/fetcher/fetcher_internal_test.go

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -26,77 +26,42 @@ func TestVerifyFeeRecipient(t *testing.T) {
2626
name: "bellatrix",
2727
proposal: eth2api.VersionedProposal{
2828
Version: eth2spec.DataVersionBellatrix,
29+
Blinded: false,
2930
Bellatrix: testutil.RandomBellatrixBeaconBlock(),
3031
},
3132
},
32-
{
33-
name: "bellatrix blinded",
34-
proposal: eth2api.VersionedProposal{
35-
Version: eth2spec.DataVersionBellatrix,
36-
BellatrixBlinded: testutil.RandomBellatrixBlindedBeaconBlock(),
37-
Blinded: true,
38-
},
39-
},
4033
{
4134
name: "capella",
4235
proposal: eth2api.VersionedProposal{
4336
Version: eth2spec.DataVersionCapella,
37+
Blinded: false,
4438
Capella: testutil.RandomCapellaBeaconBlock(),
4539
},
4640
},
47-
{
48-
name: "capella blinded",
49-
proposal: eth2api.VersionedProposal{
50-
Version: eth2spec.DataVersionCapella,
51-
CapellaBlinded: testutil.RandomCapellaBlindedBeaconBlock(),
52-
Blinded: true,
53-
},
54-
},
5541
{
5642
name: "deneb",
5743
proposal: eth2api.VersionedProposal{
5844
Version: eth2spec.DataVersionDeneb,
45+
Blinded: false,
5946
Deneb: testutil.RandomDenebVersionedProposal().Deneb,
6047
},
6148
},
62-
{
63-
name: "deneb blinded",
64-
proposal: eth2api.VersionedProposal{
65-
Version: eth2spec.DataVersionDeneb,
66-
DenebBlinded: testutil.RandomDenebBlindedBeaconBlock(),
67-
Blinded: true,
68-
},
69-
},
7049
{
7150
name: "electra",
7251
proposal: eth2api.VersionedProposal{
7352
Version: eth2spec.DataVersionElectra,
53+
Blinded: false,
7454
Electra: testutil.RandomElectraVersionedProposal().Electra,
7555
},
7656
},
77-
{
78-
name: "electra blinded",
79-
proposal: eth2api.VersionedProposal{
80-
Version: eth2spec.DataVersionElectra,
81-
ElectraBlinded: testutil.RandomElectraBlindedBeaconBlock(),
82-
Blinded: true,
83-
},
84-
},
8557
{
8658
name: "fulu",
8759
proposal: eth2api.VersionedProposal{
8860
Version: eth2spec.DataVersionFulu,
61+
Blinded: false,
8962
Fulu: testutil.RandomFuluVersionedProposal().Fulu,
9063
},
9164
},
92-
{
93-
name: "fulu blinded",
94-
proposal: eth2api.VersionedProposal{
95-
Version: eth2spec.DataVersionFulu,
96-
FuluBlinded: testutil.RandomElectraBlindedBeaconBlock(),
97-
Blinded: true,
98-
},
99-
},
10065
}
10166

10267
for _, test := range tests {

core/fetcher/metrics.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ var proposalBlindedGauge = promauto.NewGauge(prometheus.GaugeOpts{
1414
Name: "proposal_blinded",
1515
Help: "Whether the fetched proposal was blinded (1) or local (2)",
1616
})
17+
18+
var proposalLocalMismatchFeeRecipientGauge = promauto.NewGauge(prometheus.GaugeOpts{
19+
Namespace: "core",
20+
Subsystem: "fetcher",
21+
Name: "proposal_local_mismatch_fee_recipient",
22+
Help: "Counts the number of times a local proposal has a mismatched fee recipient",
23+
})

docs/metrics.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ when storing metrics from multiple nodes or clusters in one Prometheus instance.
6262
| `core_consensus_error_total` | Counter | Total count of consensus errors by protocol | `protocol` |
6363
| `core_consensus_timeout_total` | Counter | Total count of consensus timeouts by protocol, duty, and timer | `protocol, duty, timer` |
6464
| `core_fetcher_proposal_blinded` | Gauge | Whether the fetched proposal was blinded (1) or local (2) | |
65+
| `core_fetcher_proposal_local_mismatch_fee_recipient` | Gauge | Counts the number of times a local proposal has a mismatched fee recipient | |
6566
| `core_parsigdb_exit_total` | Counter | Total number of partially signed voluntary exits per public key | `pubkey` |
6667
| `core_parsigdb_store` | Histogram | Latency of partial signatures received since earliest expected time, per duty, per peer index | `duty, peer_idx` |
6768
| `core_parsigex_set_verification_seconds` | Histogram | Duration to verify all partial signatures in a received set, in seconds | `duty` |

0 commit comments

Comments
 (0)