Skip to content

Commit 26c612f

Browse files
authored
fix: make disperser client backwards compatible (#1686)
* chore(apiserver): refactor GetPaymentState to highlight legacy conversion * wip: attempt to make disperser_client backwards compatible TODO: review the Claude generated getPaymentStateFromLegacyAPI... no idea if its OK or not. * docs(apiserver): document convertAllQuorumsReplyToLegacy function * fix(disperser_client): convertLegacyPaymentStateToNew returns error * fix(disperser_client): set correct value for ReservationRateLimitWindow * style: targetting -> targeting * docs: remove outdated TODO comment * docs: add docstring for getPaymentStateFromLegacyAPI * fix: return error if PaymentGlobalParams==nil * docs(proto): document GetPaymentStateReply fields also GetPaymentStateForAllQuorumsReply fields * docs(proto): document why GetPaymentStateRequest is separate type from GetPaymentStateForAllQuorumsRequest * style: remove outdated comment * fix(disperser_client): onDemandEnabled value in convertLegacyPaymentStateToNew * style: better error message in convertLegacyPaymentStateToNew * refactor: convertLegacyPaymentStateToNew function return early when no reservation exists, which makes the logic much simpler * docs(proto): fix inaccurate cumulative_payment docstring * style(disperser_client): set reservationAdvanceWindow to 0 in convertLegacyPaymentStateToNew * fix(disp_client): convertLegacyPaymentStateToNew quorums setting logic * docs(proto): remove wrong cumulative_payment comment * style: remove unneeded comments * style: add comment explaining why append is ok * docs: move reservation_advance_window comment to proto
1 parent 335b0d7 commit 26c612f

File tree

4 files changed

+203
-10
lines changed

4 files changed

+203
-10
lines changed

api/clients/v2/disperser_client.go

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import (
1717
"github.com/Layr-Labs/eigenda/encoding/rs"
1818
"github.com/Layr-Labs/eigensdk-go/logging"
1919
"github.com/docker/go-units"
20+
gethcommon "github.com/ethereum/go-ethereum/common"
2021
"google.golang.org/grpc"
22+
"google.golang.org/grpc/codes"
23+
"google.golang.org/grpc/status"
2124
)
2225

2326
type DisperserClientConfig struct {
@@ -390,7 +393,135 @@ func (c *disperserClient) GetPaymentState(ctx context.Context) (*disperser_rpc.G
390393
Signature: signature,
391394
Timestamp: timestamp,
392395
}
393-
return c.client.GetPaymentStateForAllQuorums(ctx, request)
396+
allQuorumsReply, err := c.client.GetPaymentStateForAllQuorums(ctx, request)
397+
if err != nil {
398+
// Check if error is "method not found" or "unimplemented"
399+
if isMethodNotFoundError(err) {
400+
// Fall back to old method
401+
return c.getPaymentStateFromLegacyAPI(ctx, accountID, signature, timestamp)
402+
}
403+
return nil, err
404+
}
405+
406+
return allQuorumsReply, nil
407+
}
408+
409+
// this is true if we are targeting a disperser that hasn't upgraded to the new API yet.
410+
func isMethodNotFoundError(err error) bool {
411+
if st, ok := status.FromError(err); ok {
412+
return st.Code() == codes.Unimplemented
413+
}
414+
return false
415+
}
416+
417+
// getPaymentStateFromLegacyAPI retrieves the payment state from the legacy GetPaymentState grpc method.
418+
// It is needed until we have upgraded all dispersers (testnet and mainnet) to the new API.
419+
// Check those endpoints for GetPaymentStateForAllQuorums using:
420+
// `grpcurl disperser-testnet-holesky.eigenda.xyz:443 list disperser.v2.Disperser`
421+
// `grpcurl disperser.eigenda.xyz:443 list disperser.v2.Disperser`
422+
func (c *disperserClient) getPaymentStateFromLegacyAPI(
423+
ctx context.Context, accountID gethcommon.Address, signature []byte, timestamp uint64,
424+
) (*disperser_rpc.GetPaymentStateForAllQuorumsReply, error) {
425+
oldRequest := &disperser_rpc.GetPaymentStateRequest{
426+
AccountId: accountID.Hex(),
427+
Signature: signature,
428+
Timestamp: timestamp,
429+
}
430+
431+
oldResult, err := c.client.GetPaymentState(ctx, oldRequest)
432+
if err != nil {
433+
return nil, err
434+
}
435+
436+
return convertLegacyPaymentStateToNew(oldResult)
437+
}
438+
439+
// convertLegacyPaymentStateToNew converts the old GetPaymentStateReply to the new GetPaymentStateForAllQuorumsReply format
440+
func convertLegacyPaymentStateToNew(legacyReply *disperser_rpc.GetPaymentStateReply) (*disperser_rpc.GetPaymentStateForAllQuorumsReply, error) {
441+
442+
if legacyReply.PaymentGlobalParams == nil {
443+
return nil, fmt.Errorf("legacy payment state received from disperser does not contain global params")
444+
}
445+
// Convert PaymentGlobalParams to PaymentVaultParams
446+
var paymentVaultParams *disperser_rpc.PaymentVaultParams
447+
{
448+
paymentVaultParams = &disperser_rpc.PaymentVaultParams{
449+
QuorumPaymentConfigs: make(map[uint32]*disperser_rpc.PaymentQuorumConfig),
450+
QuorumProtocolConfigs: make(map[uint32]*disperser_rpc.PaymentQuorumProtocolConfig),
451+
OnDemandQuorumNumbers: legacyReply.PaymentGlobalParams.OnDemandQuorumNumbers,
452+
}
453+
454+
// Apply the global params to all quorums, both on-demand and reservation.
455+
onDemandQuorums := legacyReply.PaymentGlobalParams.OnDemandQuorumNumbers
456+
if len(onDemandQuorums) == 0 {
457+
return nil, fmt.Errorf("no on-demand quorums specified in legacy PaymentGlobalParams received from disperser")
458+
}
459+
reservationQuorums := legacyReply.Reservation.QuorumNumbers
460+
// There may be overlapping quorums but it doesn't matter since we will apply the same global params to all of them.
461+
allQuorums := append(reservationQuorums, onDemandQuorums...)
462+
463+
for _, quorumID := range allQuorums {
464+
paymentVaultParams.QuorumPaymentConfigs[quorumID] = &disperser_rpc.PaymentQuorumConfig{
465+
ReservationSymbolsPerSecond: 0, // Not available in legacy format
466+
OnDemandSymbolsPerSecond: legacyReply.PaymentGlobalParams.GlobalSymbolsPerSecond,
467+
OnDemandPricePerSymbol: legacyReply.PaymentGlobalParams.PricePerSymbol,
468+
}
469+
470+
paymentVaultParams.QuorumProtocolConfigs[quorumID] = &disperser_rpc.PaymentQuorumProtocolConfig{
471+
MinNumSymbols: legacyReply.PaymentGlobalParams.MinNumSymbols,
472+
// ReservationAdvanceWindow is not used offchain at the moment so it's okay to set to any value.
473+
ReservationAdvanceWindow: 0,
474+
ReservationRateLimitWindow: legacyReply.PaymentGlobalParams.ReservationWindow,
475+
OnDemandRateLimitWindow: 0, // Not available in legacy format
476+
}
477+
}
478+
479+
for _, quorumID := range onDemandQuorums {
480+
paymentVaultParams.QuorumProtocolConfigs[quorumID].OnDemandEnabled = true
481+
}
482+
}
483+
484+
// If no reservation is available, return early with only payment vault params and cumulative payment info.
485+
if legacyReply.Reservation == nil {
486+
return &disperser_rpc.GetPaymentStateForAllQuorumsReply{
487+
PaymentVaultParams: paymentVaultParams,
488+
CumulativePayment: legacyReply.CumulativePayment,
489+
OnchainCumulativePayment: legacyReply.OnchainCumulativePayment,
490+
}, nil
491+
}
492+
493+
// Otherwise there is a reservation available, so we need to convert it to the per-quorum format.
494+
495+
// We first make sure that the disperser returned valid data.
496+
if len(legacyReply.PeriodRecords) == 0 {
497+
return nil, fmt.Errorf("legacy payment state received from disperser does not contain period records")
498+
}
499+
if len(legacyReply.Reservation.QuorumNumbers) == 0 {
500+
return nil, fmt.Errorf("legacy payment state received from disperser does not contain reservation quorums")
501+
}
502+
503+
reservations := make(map[uint32]*disperser_rpc.QuorumReservation)
504+
periodRecords := make(map[uint32]*disperser_rpc.PeriodRecords)
505+
506+
// Apply the reservation to all reservationQuorums mentioned in the reservation
507+
for _, quorumID := range legacyReply.Reservation.QuorumNumbers {
508+
reservations[quorumID] = &disperser_rpc.QuorumReservation{
509+
SymbolsPerSecond: legacyReply.Reservation.SymbolsPerSecond,
510+
StartTimestamp: legacyReply.Reservation.StartTimestamp,
511+
EndTimestamp: legacyReply.Reservation.EndTimestamp,
512+
}
513+
periodRecords[quorumID] = &disperser_rpc.PeriodRecords{
514+
Records: legacyReply.PeriodRecords,
515+
}
516+
}
517+
518+
return &disperser_rpc.GetPaymentStateForAllQuorumsReply{
519+
PaymentVaultParams: paymentVaultParams,
520+
PeriodRecords: periodRecords,
521+
Reservations: reservations,
522+
CumulativePayment: legacyReply.CumulativePayment,
523+
OnchainCumulativePayment: legacyReply.OnchainCumulativePayment,
524+
}, nil
394525
}
395526

396527
// GetBlobCommitment is a utility method that calculates commitment for a blob payload.

api/grpc/disperser/v2/disperser_v2.pb.go

Lines changed: 27 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/proto/disperser/v2/disperser_v2.proto

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ message BlobCommitmentReply {
134134
}
135135

136136
// GetPaymentStateRequest contains parameters to query the payment state of an account.
137+
// GetPaymentStateForAllQuorumsRequest is a separate message type even though it currently contains the same fields,
138+
// because we follow buf's best practices and linting rules which recommend every RPC having its own request and reply types,
139+
// to allow for evolution of the API without breaking changes: https://buf.build/docs/lint/rules/#rpc_no_server_streaming
137140
message GetPaymentStateRequest {
138141
// The ID of the account being queried. This account ID is an eth wallet address of the user.
139142
string account_id = 1;
@@ -160,12 +163,21 @@ message GetPaymentStateReply {
160163
// global payment vault parameters
161164
PaymentGlobalParams payment_global_params = 1;
162165
// off-chain account reservation usage records
166+
// Should be empty if reservation.quorum_numbers is empty (i.e. no reservation exists for the account).
163167
repeated PeriodRecord period_records = 2;
164168
// on-chain account reservation setting
165169
Reservation reservation = 3;
166-
// off-chain on-demand payment usage
170+
// off-chain on-demand payment usage.
171+
// The bytes are parsed to a big.Int value.
172+
// This value should always be <= onchain_cumulative_payment.
173+
// See [common.v2.PaymentHeader.cumulative_payment] for more details.
174+
//
175+
// This value should only be nonzero for the EigenLabs disperser, as it is the only disperser that supports on-demand payments currently.
176+
// Future work will support decentralized on-demand dispersals.
167177
bytes cumulative_payment = 4;
168178
// on-chain on-demand payment deposited
179+
// The bytes are parsed to a big.Int value.
180+
// See [common.v2.PaymentHeader.cumulative_payment] for more details.
169181
bytes onchain_cumulative_payment = 5;
170182
}
171183

@@ -178,14 +190,23 @@ message GetPaymentStateForAllQuorumsReply {
178190
// payment vault parameters with per-quorum configurations
179191
PaymentVaultParams payment_vault_params = 1;
180192
// period_records maps quorum IDs to the off-chain account reservation usage records for the current and next two periods
193+
// Should contain the same number of entries as the `reservations` field.
181194
map<uint32, PeriodRecords> period_records = 2;
182195
// reservations maps quorum IDs to the on-chain account reservation record
196+
// Should contain the same number of entries as the `period_records` field.
183197
map<uint32, QuorumReservation> reservations = 3;
184-
// off-chain on-demand payment usage. This field is currently only tracked by EigenLabs disperser because on-demand requests are only
185-
// supported by EigenLabs. Future work will support decentralized on-demand dispersals and this field later be tracked and shared by
186-
// dispersers unlimited to EigenLabs.
198+
// off-chain on-demand payment usage.
199+
// The bytes are parsed to a big.Int value.
200+
// This value should always be <= onchain_cumulative_payment.
201+
// See [common.v2.PaymentHeader.cumulative_payment] for more details.
202+
//
203+
// This value should only be nonzero for the EigenLabs disperser, as it is the only disperser that supports on-demand payments currently.
204+
// Future work will support decentralized on-demand dispersals.
187205
bytes cumulative_payment = 4;
188206
// on-chain on-demand payment deposited.
207+
// The bytes are parsed to a big.Int value.
208+
// This value should always be >= cumulative_payment.
209+
// See [common.v2.PaymentHeader.cumulative_payment] for more details.
189210
bytes onchain_cumulative_payment = 5;
190211
}
191212

@@ -341,6 +362,8 @@ message PaymentQuorumProtocolConfig {
341362
uint64 min_num_symbols = 1;
342363

343364
// reservation_advance_window is the window in seconds before a reservation starts that it can be activated
365+
// It is added here for offchain to have access to all onchain data structs, but it isn't currently used,
366+
// and might get removed in the future.
344367
uint64 reservation_advance_window = 2;
345368

346369
// reservation_rate_limit_window is the time window in seconds for reservation rate limiting

disperser/apiserver/server_v2.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,22 @@ func (s *DispersalServerV2) GetPaymentState(ctx context.Context, req *pb.GetPaym
290290
return nil, err
291291
}
292292

293+
return convertAllQuorumsReplyToLegacy(allQuorumsReply), nil
294+
}
295+
296+
// convertAllQuorumsReplyToLegacy converts the new per-quorum payment state format to the legacy aggregated format.
297+
// This enables backwards compatibility by flattening multi-quorum data into a single legacy response,
298+
// allowing old clients to continue working properly.
299+
//
300+
// Conversion logic:
301+
// - PaymentVaultParams: Uses quorum 0 configuration as the global parameters (arbitrary choice)
302+
// - Reservations: Finds the most restrictive reservation across all quorums (minimum symbols/sec, latest start, earliest end)
303+
// - PeriodRecords: Selects the highest usage for each period index across all quorums
304+
// - OnDemand cumulative payment amounts: Passed through unchanged as they represent account-level totals
305+
//
306+
// This conversion may result in information loss for clients that need per-quorum details,
307+
// but preserves the most restrictive constraints to ensure client behavior remains within reservation or ondemand.
308+
func convertAllQuorumsReplyToLegacy(allQuorumsReply *pb.GetPaymentStateForAllQuorumsReply) *pb.GetPaymentStateReply {
293309
// For PaymentVaultParams, use quorum 0 for protocol level parameters and on-demand quorum numbers
294310
var paymentGlobalParams *pb.PaymentGlobalParams
295311
if allQuorumsReply.PaymentVaultParams != nil &&
@@ -376,7 +392,7 @@ func (s *DispersalServerV2) GetPaymentState(ctx context.Context, req *pb.GetPaym
376392
Reservation: reservation,
377393
CumulativePayment: allQuorumsReply.CumulativePayment,
378394
OnchainCumulativePayment: allQuorumsReply.OnchainCumulativePayment,
379-
}, nil
395+
}
380396
}
381397

382398
// GetPaymentStateForAllQuorums returns payment state for all quorums including vault parameters,

0 commit comments

Comments
 (0)