Skip to content

Commit 36b5188

Browse files
coordinator: validate location before marshaling
UpsertCannonLocation and UpsertRelayMonitorLocation dereferenced the incoming location without checking for nil, so a request with the field omitted crashed the whole server. Both handlers now reject a nil location with InvalidArgument before touching it. Marshal also accepted a request where the type was set but the matching data payload was missing, and silently wrote an empty marker instead of the real value. That marker looks like a valid location to every reader downstream, so it quietly resets whatever checkpoint or cursor the entry represented. Both the cannon and relay monitor Marshal implementations now reject this case with a new ErrLocationDataRequired error. The cannon implementation also had several types marshaling inline instead of going through the shared helper, so those were consolidated to keep the check in one place. Added a recovery interceptor to the grpc server so a panic in a handler returns an internal error to the caller instead of taking down the process. This is a backstop for bugs like the one above, not a replacement for validating input.
1 parent fc20a34 commit 36b5188

7 files changed

Lines changed: 336 additions & 192 deletions

File tree

pkg/server/persistence/cannon/location.go

Lines changed: 28 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,21 @@ type Location struct {
2727
}
2828

2929
var (
30-
ErrFailedToMarshal = errors.New("failed to marshal location")
31-
ErrFailedToUnmarshal = errors.New("failed to unmarshal location")
30+
ErrFailedToMarshal = errors.New("failed to marshal location")
31+
ErrFailedToUnmarshal = errors.New("failed to unmarshal location")
32+
ErrLocationDataRequired = errors.New("location data is required")
3233
)
3334

3435
// marshalLocationData protojson-marshals data into l.Value and sets l.Type.
36+
// data must be a non-nil, populated message: an absent oneof value (Type set
37+
// but its corresponding Data variant not populated) is rejected rather than
38+
// silently written as an empty "{}" marker, which would otherwise reset
39+
// whatever cursor/progress this location represents.
3540
func (l *Location) marshalLocationData(typ string, data proto.Message) error {
41+
if data == nil || !data.ProtoReflect().IsValid() {
42+
return fmt.Errorf("%w: type %s", ErrLocationDataRequired, typ)
43+
}
44+
3645
l.Type = typ
3746

3847
b, err := protojson.Marshal(data)
@@ -60,174 +69,35 @@ func (l *Location) Marshal(msg *xatu.CannonLocation) error {
6069

6170
switch msg.Type {
6271
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_VOLUNTARY_EXIT:
63-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_VOLUNTARY_EXIT"
64-
65-
data := msg.GetEthV2BeaconBlockVoluntaryExit()
66-
67-
b, err := protojson.Marshal(data)
68-
if err != nil {
69-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
70-
}
71-
72-
l.Value = string(b)
72+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_VOLUNTARY_EXIT", msg.GetEthV2BeaconBlockVoluntaryExit())
7373
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_PROPOSER_SLASHING:
74-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_PROPOSER_SLASHING"
75-
76-
data := msg.GetEthV2BeaconBlockProposerSlashing()
77-
78-
b, err := protojson.Marshal(data)
79-
if err != nil {
80-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
81-
}
82-
83-
l.Value = string(b)
74+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_PROPOSER_SLASHING", msg.GetEthV2BeaconBlockProposerSlashing())
8475
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_DEPOSIT:
85-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_DEPOSIT"
86-
87-
data := msg.GetEthV2BeaconBlockDeposit()
88-
89-
b, err := protojson.Marshal(data)
90-
if err != nil {
91-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
92-
}
93-
94-
l.Value = string(b)
76+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_DEPOSIT", msg.GetEthV2BeaconBlockDeposit())
9577
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_ATTESTER_SLASHING:
96-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_ATTESTER_SLASHING"
97-
98-
data := msg.GetEthV2BeaconBlockAttesterSlashing()
99-
100-
b, err := protojson.Marshal(data)
101-
if err != nil {
102-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
103-
}
104-
105-
l.Value = string(b)
106-
78+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_ATTESTER_SLASHING", msg.GetEthV2BeaconBlockAttesterSlashing())
10779
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_EXECUTION_TRANSACTION:
108-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_EXECUTION_TRANSACTION"
109-
110-
data := msg.GetEthV2BeaconBlockExecutionTransaction()
111-
112-
b, err := protojson.Marshal(data)
113-
if err != nil {
114-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
115-
}
116-
117-
l.Value = string(b)
118-
80+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_EXECUTION_TRANSACTION", msg.GetEthV2BeaconBlockExecutionTransaction())
11981
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_BLS_TO_EXECUTION_CHANGE:
120-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_BLS_TO_EXECUTION_CHANGE"
121-
122-
data := msg.GetEthV2BeaconBlockBlsToExecutionChange()
123-
124-
b, err := protojson.Marshal(data)
125-
if err != nil {
126-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
127-
}
128-
129-
l.Value = string(b)
82+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_BLS_TO_EXECUTION_CHANGE", msg.GetEthV2BeaconBlockBlsToExecutionChange())
13083
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_WITHDRAWAL:
131-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_WITHDRAWAL"
132-
133-
data := msg.GetEthV2BeaconBlockWithdrawal()
134-
135-
b, err := protojson.Marshal(data)
136-
if err != nil {
137-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
138-
}
139-
140-
l.Value = string(b)
141-
84+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_WITHDRAWAL", msg.GetEthV2BeaconBlockWithdrawal())
14285
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK:
143-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK"
144-
145-
data := msg.GetEthV2BeaconBlock()
146-
147-
b, err := protojson.Marshal(data)
148-
if err != nil {
149-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
150-
}
151-
152-
l.Value = string(b)
153-
86+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK", msg.GetEthV2BeaconBlock())
15487
case xatu.CannonType_BEACON_API_ETH_V1_BEACON_BLOB_SIDECAR:
155-
l.Type = "BEACON_API_ETH_V1_BEACON_BLOB_SIDECAR"
156-
157-
data := msg.GetEthV1BeaconBlobSidecar()
158-
159-
b, err := protojson.Marshal(data)
160-
if err != nil {
161-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
162-
}
163-
164-
l.Value = string(b)
88+
return l.marshalLocationData("BEACON_API_ETH_V1_BEACON_BLOB_SIDECAR", msg.GetEthV1BeaconBlobSidecar())
16589
case xatu.CannonType_BEACON_API_ETH_V1_PROPOSER_DUTY:
166-
l.Type = "BEACON_API_ETH_V1_PROPOSER_DUTY"
167-
168-
data := msg.GetEthV1BeaconProposerDuty()
169-
170-
b, err := protojson.Marshal(data)
171-
if err != nil {
172-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
173-
}
174-
175-
l.Value = string(b)
90+
return l.marshalLocationData("BEACON_API_ETH_V1_PROPOSER_DUTY", msg.GetEthV1BeaconProposerDuty())
17691
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_ELABORATED_ATTESTATION:
177-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_ELABORATED_ATTESTATION"
178-
179-
data := msg.GetEthV2BeaconBlockElaboratedAttestation()
180-
181-
b, err := protojson.Marshal(data)
182-
if err != nil {
183-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
184-
}
185-
186-
l.Value = string(b)
92+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_ELABORATED_ATTESTATION", msg.GetEthV2BeaconBlockElaboratedAttestation())
18793
case xatu.CannonType_BEACON_API_ETH_V1_BEACON_VALIDATORS:
188-
l.Type = "BEACON_API_ETH_V1_BEACON_VALIDATORS"
189-
190-
data := msg.GetEthV1BeaconValidators()
191-
192-
b, err := protojson.Marshal(data)
193-
if err != nil {
194-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
195-
}
196-
197-
l.Value = string(b)
94+
return l.marshalLocationData("BEACON_API_ETH_V1_BEACON_VALIDATORS", msg.GetEthV1BeaconValidators())
19895
case xatu.CannonType_BEACON_API_ETH_V1_BEACON_COMMITTEE:
199-
l.Type = "BEACON_API_ETH_V1_BEACON_COMMITTEE"
200-
201-
data := msg.GetEthV1BeaconCommittee()
202-
203-
b, err := protojson.Marshal(data)
204-
if err != nil {
205-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
206-
}
207-
208-
l.Value = string(b)
96+
return l.marshalLocationData("BEACON_API_ETH_V1_BEACON_COMMITTEE", msg.GetEthV1BeaconCommittee())
20997
case xatu.CannonType_BEACON_API_ETH_V1_BEACON_SYNC_COMMITTEE:
210-
l.Type = "BEACON_API_ETH_V1_BEACON_SYNC_COMMITTEE"
211-
212-
data := msg.GetEthV1BeaconSyncCommittee()
213-
214-
b, err := protojson.Marshal(data)
215-
if err != nil {
216-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
217-
}
218-
219-
l.Value = string(b)
98+
return l.marshalLocationData("BEACON_API_ETH_V1_BEACON_SYNC_COMMITTEE", msg.GetEthV1BeaconSyncCommittee())
22099
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_SYNC_AGGREGATE:
221-
l.Type = "BEACON_API_ETH_V2_BEACON_BLOCK_SYNC_AGGREGATE"
222-
223-
data := msg.GetEthV2BeaconBlockSyncAggregate()
224-
225-
b, err := protojson.Marshal(data)
226-
if err != nil {
227-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
228-
}
229-
230-
l.Value = string(b)
100+
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_SYNC_AGGREGATE", msg.GetEthV2BeaconBlockSyncAggregate())
231101
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_EXECUTION_REQUEST_DEPOSIT:
232102
return l.marshalLocationData("BEACON_API_ETH_V2_BEACON_BLOCK_EXECUTION_REQUEST_DEPOSIT", msg.GetEthV2BeaconBlockExecutionRequestDeposit())
233103
case xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_EXECUTION_REQUEST_WITHDRAWAL:
@@ -251,27 +121,9 @@ func (l *Location) Marshal(msg *xatu.CannonLocation) error {
251121
case xatu.CannonType_BEACON_API_ETH_V1_BEACON_STATE_PENDING_CONSOLIDATION:
252122
return l.marshalLocationData("BEACON_API_ETH_V1_BEACON_STATE_PENDING_CONSOLIDATION", msg.GetEthV1BeaconStatePendingConsolidation())
253123
case xatu.CannonType_EXECUTION_CANONICAL_BLOCK:
254-
l.Type = "EXECUTION_CANONICAL_BLOCK"
255-
256-
data := msg.GetExecutionCanonicalBlock()
257-
258-
b, err := protojson.Marshal(data)
259-
if err != nil {
260-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
261-
}
262-
263-
l.Value = string(b)
124+
return l.marshalLocationData("EXECUTION_CANONICAL_BLOCK", msg.GetExecutionCanonicalBlock())
264125
case xatu.CannonType_EXECUTION_CANONICAL_TRANSACTION:
265-
l.Type = "EXECUTION_CANONICAL_TRANSACTION"
266-
267-
data := msg.GetExecutionCanonicalTransaction()
268-
269-
b, err := protojson.Marshal(data)
270-
if err != nil {
271-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
272-
}
273-
274-
l.Value = string(b)
126+
return l.marshalLocationData("EXECUTION_CANONICAL_TRANSACTION", msg.GetExecutionCanonicalTransaction())
275127
case xatu.CannonType_EXECUTION_CANONICAL_LOGS:
276128
return l.marshalLocationData("EXECUTION_CANONICAL_LOGS", msg.GetExecutionCanonicalLogs())
277129
case xatu.CannonType_EXECUTION_CANONICAL_TRACES:
@@ -303,8 +155,6 @@ func (l *Location) Marshal(msg *xatu.CannonLocation) error {
303155
default:
304156
return fmt.Errorf("unknown type: %s", msg.Type)
305157
}
306-
307-
return nil
308158
}
309159

310160
func (l *Location) Unmarshal() (*xatu.CannonLocation, error) {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package cannon
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/ethpandaops/xatu/pkg/proto/xatu"
8+
)
9+
10+
// A request with Type set but the Data oneof left empty is a legal wire
11+
// message. Marshal used to accept it and silently write an empty JSON
12+
// object as the value, which reset whatever checkpoint that location
13+
// represented. It should now reject the request instead.
14+
func TestMarshal_TypeSetDataAbsent_ReturnsError(t *testing.T) {
15+
msg := &xatu.CannonLocation{
16+
NetworkId: "mainnet",
17+
Type: xatu.CannonType_BEACON_API_ETH_V2_BEACON_BLOCK_VOLUNTARY_EXIT,
18+
}
19+
20+
l := &Location{}
21+
22+
err := l.Marshal(msg)
23+
if err == nil {
24+
t.Fatalf("expected an error, got nil (l.Value = %q)", l.Value)
25+
}
26+
27+
if !errors.Is(err, ErrLocationDataRequired) {
28+
t.Fatalf("expected ErrLocationDataRequired, got: %v", err)
29+
}
30+
31+
if l.Value != "" {
32+
t.Fatalf("l.Value should be untouched on rejection, got %q", l.Value)
33+
}
34+
}
35+
36+
// Every Type-set/Data-absent case is expected to go through the same
37+
// marshalLocationData check, so a few representative types are covered
38+
// explicitly here too.
39+
func TestMarshal_TypeSetDataAbsent_ReturnsError_AcrossTypes(t *testing.T) {
40+
cases := []struct {
41+
name string
42+
typ xatu.CannonType
43+
}{
44+
{"EXECUTION_CANONICAL_BLOCK", xatu.CannonType_EXECUTION_CANONICAL_BLOCK},
45+
{"EXECUTION_CANONICAL_TRANSACTION", xatu.CannonType_EXECUTION_CANONICAL_TRANSACTION},
46+
{"BEACON_API_ETH_V1_BEACON_STATE_FINALITY_CHECKPOINT", xatu.CannonType_BEACON_API_ETH_V1_BEACON_STATE_FINALITY_CHECKPOINT},
47+
}
48+
49+
for _, tc := range cases {
50+
t.Run(tc.name, func(t *testing.T) {
51+
l := &Location{}
52+
53+
err := l.Marshal(&xatu.CannonLocation{NetworkId: "mainnet", Type: tc.typ})
54+
if !errors.Is(err, ErrLocationDataRequired) {
55+
t.Fatalf("expected ErrLocationDataRequired for %s, got: %v", tc.name, err)
56+
}
57+
})
58+
}
59+
}
60+
61+
// Rows written before this validation existed may still contain an empty
62+
// marker. Unmarshal should keep reading them back without error, since
63+
// there is no migration to clean up historical rows.
64+
func TestUnmarshalOfLegacyEmptyMarker_StillReadsBackSafely(t *testing.T) {
65+
l := &Location{
66+
NetworkID: "mainnet",
67+
Type: "BEACON_API_ETH_V2_BEACON_BLOCK_VOLUNTARY_EXIT",
68+
Value: "{}",
69+
}
70+
71+
msg, err := l.Unmarshal()
72+
if err != nil {
73+
t.Fatalf("Unmarshal returned an error reading legacy data: %v", err)
74+
}
75+
76+
data := msg.GetEthV2BeaconBlockVoluntaryExit()
77+
if data == nil {
78+
t.Fatal("expected a non-nil, zero-valued VoluntaryExit data message")
79+
}
80+
}

pkg/server/persistence/relaymonitor/location.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ type Location struct {
3030
}
3131

3232
var (
33-
ErrFailedToMarshal = errors.New("failed to marshal location")
34-
ErrFailedToUnmarshal = errors.New("failed to unmarshal location")
33+
ErrFailedToMarshal = errors.New("failed to marshal location")
34+
ErrFailedToUnmarshal = errors.New("failed to unmarshal location")
35+
ErrLocationDataRequired = errors.New("location data is required")
3536
)
3637

3738
// Marshal marshals a proto message into the Location fields.
@@ -45,28 +46,32 @@ func (l *Location) Marshal(msg *xatu.RelayMonitorLocation) error {
4546
l.Type = "RELAY_MONITOR_BID_TRACE"
4647

4748
data := msg.GetBidTrace()
48-
if data != nil {
49-
b, err := protojson.Marshal(data)
50-
if err != nil {
51-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
52-
}
49+
if data == nil || !data.ProtoReflect().IsValid() {
50+
return fmt.Errorf("%w: type %s", ErrLocationDataRequired, l.Type)
51+
}
5352

54-
l.Value = string(b)
53+
b, err := protojson.Marshal(data)
54+
if err != nil {
55+
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
5556
}
5657

58+
l.Value = string(b)
59+
5760
case xatu.RelayMonitorType_RELAY_MONITOR_PAYLOAD_DELIVERED:
5861
l.Type = "RELAY_MONITOR_PAYLOAD_DELIVERED"
5962

6063
data := msg.GetPayloadDelivered()
61-
if data != nil {
62-
b, err := protojson.Marshal(data)
63-
if err != nil {
64-
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
65-
}
64+
if data == nil || !data.ProtoReflect().IsValid() {
65+
return fmt.Errorf("%w: type %s", ErrLocationDataRequired, l.Type)
66+
}
6667

67-
l.Value = string(b)
68+
b, err := protojson.Marshal(data)
69+
if err != nil {
70+
return fmt.Errorf("%w: %s", ErrFailedToMarshal, err)
6871
}
6972

73+
l.Value = string(b)
74+
7075
default:
7176
return fmt.Errorf("unknown type: %s", msg.Type)
7277
}

0 commit comments

Comments
 (0)