Skip to content

Commit

Permalink
[payments] disperser server metering (#792)
Browse files Browse the repository at this point in the history
  • Loading branch information
hopeyen authored Oct 30, 2024
1 parent 28f1709 commit 43d8c54
Show file tree
Hide file tree
Showing 17 changed files with 969 additions and 318 deletions.
327 changes: 157 additions & 170 deletions api/grpc/disperser/disperser.pb.go

Large diffs are not rendered by default.

18 changes: 3 additions & 15 deletions api/proto/disperser/disperser.proto
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,10 @@ message DisperseBlobRequest {
}

message DispersePaidBlobRequest {
// The data to be dispersed.
// The size of data must be <= 2MiB. Every 32 bytes of data chunk is interpreted as an integer in big endian format
// where the lower address has more significant bits. The integer must stay in the valid range to be interpreted
// as a field element on the bn254 curve. The valid range is
// 0 <= x < 21888242871839275222246405745257275088548364400416034343698204186575808495617
// containing slightly less than 254 bits and more than 253 bits. If any one of the 32 bytes chunk is outside the range,
// the whole request is deemed as invalid, and rejected.
// NOTE: I want to include dataLength here, not the data itself.
// The data to be dispersed. Same requirements as DisperseBlobRequest.
bytes data = 1;
// The quorums to which the blob will be sent, in addition to the required quorums which are configured
// on the EigenDA smart contract. If required quorums are included here, an error will be returned.
// The disperser will ensure that the encoded blobs for each quorum are all processed
// within the same batch. The request doesn't need to include the payment split because the information is registered on-chain.
// In theory the quorum numbers should be the same as the ones in the DisperseBlobRequest, but I'm allowing freedom
// for individual requests.
repeated uint32 custom_quorum_numbers = 2;
// The quorums to which the blob to be sent
repeated uint32 quorum_numbers = 2;

// Payment header contains AccountID, BinIndex, and CumulativePayment
common.PaymentHeader payment_header = 3;
Expand Down
5 changes: 5 additions & 0 deletions core/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"

commonpb "github.com/Layr-Labs/eigenda/api/grpc/common"
geth "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
Expand Down Expand Up @@ -47,5 +48,9 @@ func VerifySignature(message []byte, accountAddr geth.Address, sig []byte) error
}

return nil
}

type PaymentSigner interface {
SignBlobPayment(header *commonpb.PaymentHeader) ([]byte, error)
GetAccountID() string
}
98 changes: 98 additions & 0 deletions core/auth/payment_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package auth

import (
"crypto/ecdsa"
"fmt"
"log"

commonpb "github.com/Layr-Labs/eigenda/api/grpc/common"
"github.com/Layr-Labs/eigenda/core"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

type PaymentSigner struct {
PrivateKey *ecdsa.PrivateKey
}

var _ core.PaymentSigner = &PaymentSigner{}

func NewPaymentSigner(privateKeyHex string) *PaymentSigner {

privateKeyBytes := common.FromHex(privateKeyHex)
privateKey, err := crypto.ToECDSA(privateKeyBytes)
if err != nil {
log.Fatalf("Failed to parse private key: %v", err)
}

return &PaymentSigner{
PrivateKey: privateKey,
}
}

// SignBlobPayment signs the payment header and returns the signature
func (s *PaymentSigner) SignBlobPayment(header *commonpb.PaymentHeader) ([]byte, error) {
header.AccountId = s.GetAccountID()
pm := core.ConvertPaymentHeader(header)
hash, err := pm.Hash()
if err != nil {
return nil, fmt.Errorf("failed to hash payment header: %v", err)
}

sig, err := crypto.Sign(hash[:], s.PrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign hash: %v", err)
}

return sig, nil
}

type NoopPaymentSigner struct{}

func NewNoopPaymentSigner() *NoopPaymentSigner {
return &NoopPaymentSigner{}
}

func (s *NoopPaymentSigner) SignBlobPayment(header *commonpb.PaymentHeader) ([]byte, error) {
return nil, fmt.Errorf("noop signer cannot sign blob payment header")
}

func (s *NoopPaymentSigner) GetAccountID() string {
return ""
}

// VerifyPaymentSignature verifies the signature against the payment metadata
func VerifyPaymentSignature(paymentHeader *commonpb.PaymentHeader, paymentSignature []byte) bool {
pm := core.ConvertPaymentHeader(paymentHeader)
hash, err := pm.Hash()
if err != nil {
return false
}

recoveredPubKey, err := crypto.SigToPub(hash[:], paymentSignature)
if err != nil {
log.Printf("Failed to recover public key from signature: %v\n", err)
return false
}

recoveredAddress := crypto.PubkeyToAddress(*recoveredPubKey)
accountId := common.HexToAddress(paymentHeader.AccountId)
if recoveredAddress != accountId {
log.Printf("Signature address %s does not match account id %s\n", recoveredAddress.Hex(), accountId.Hex())
return false
}

return crypto.VerifySignature(
crypto.FromECDSAPub(recoveredPubKey),
hash[:],
paymentSignature[:len(paymentSignature)-1], // Remove recovery ID
)
}

// GetAccountID returns the Ethereum address of the signer
func (s *PaymentSigner) GetAccountID() string {
publicKey := crypto.FromECDSAPub(&s.PrivateKey.PublicKey)
hash := crypto.Keccak256(publicKey[1:])

return common.BytesToAddress(hash[12:]).Hex()
}
76 changes: 76 additions & 0 deletions core/auth/payment_signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package auth_test

import (
"encoding/hex"
"testing"

commonpb "github.com/Layr-Labs/eigenda/api/grpc/common"
"github.com/Layr-Labs/eigenda/core/auth"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPaymentSigner(t *testing.T) {
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)

privateKeyHex := hex.EncodeToString(crypto.FromECDSA(privateKey))
signer := auth.NewPaymentSigner(privateKeyHex)

t.Run("SignBlobPayment", func(t *testing.T) {
header := &commonpb.PaymentHeader{
BinIndex: 1,
CumulativePayment: []byte{0x01, 0x02, 0x03},
AccountId: "",
}

signature, err := signer.SignBlobPayment(header)
require.NoError(t, err)
assert.NotEmpty(t, signature)

// Verify the signature
isValid := auth.VerifyPaymentSignature(header, signature)
assert.True(t, isValid)
})

t.Run("VerifyPaymentSignature_InvalidSignature", func(t *testing.T) {
header := &commonpb.PaymentHeader{
BinIndex: 1,
CumulativePayment: []byte{0x01, 0x02, 0x03},
AccountId: "",
}

// Create an invalid signature
invalidSignature := make([]byte, 65)
isValid := auth.VerifyPaymentSignature(header, invalidSignature)
assert.False(t, isValid)
})

t.Run("VerifyPaymentSignature_ModifiedHeader", func(t *testing.T) {
header := &commonpb.PaymentHeader{
BinIndex: 1,
CumulativePayment: []byte{0x01, 0x02, 0x03},
AccountId: "",
}

signature, err := signer.SignBlobPayment(header)
require.NoError(t, err)

// Modify the header after signing
header.BinIndex = 2

isValid := auth.VerifyPaymentSignature(header, signature)
assert.False(t, isValid)
})
}

func TestNoopPaymentSigner(t *testing.T) {
signer := auth.NewNoopPaymentSigner()

t.Run("SignBlobRequest", func(t *testing.T) {
_, err := signer.SignBlobPayment(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "noop signer cannot sign blob payment header")
})
}
10 changes: 10 additions & 0 deletions core/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math/big"
"strconv"

commonpb "github.com/Layr-Labs/eigenda/api/grpc/common"
"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/encoding"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
Expand Down Expand Up @@ -562,6 +563,15 @@ func (pm *PaymentMetadata) UnmarshalDynamoDBAttributeValue(av types.AttributeVal
return nil
}

// ConvertPaymentHeader converts a protobuf payment header to a PaymentMetadata
func ConvertPaymentHeader(header *commonpb.PaymentHeader) *PaymentMetadata {
return &PaymentMetadata{
AccountID: header.AccountId,
BinIndex: header.BinIndex,
CumulativePayment: new(big.Int).SetBytes(header.CumulativePayment),
}
}

// OperatorInfo contains information about an operator which is stored on the blockchain state,
// corresponding to a particular quorum
type ActiveReservation struct {
Expand Down
20 changes: 20 additions & 0 deletions core/eth/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,3 +599,23 @@ func (t *Reader) GetOnDemandPaymentByAccount(ctx context.Context, blockNumber ui
// contract is not implemented yet
return core.OnDemandPayment{}, nil
}

func (t *Reader) GetGlobalSymbolsPerSecond(ctx context.Context) (uint64, error) {
// contract is not implemented yet
return 0, nil
}

func (t *Reader) GetMinNumSymbols(ctx context.Context) (uint32, error) {
// contract is not implemented yet
return 0, nil
}

func (t *Reader) GetPricePerSymbol(ctx context.Context) (uint32, error) {
// contract is not implemented yet
return 0, nil
}

func (t *Reader) GetReservationWindow(ctx context.Context) (uint32, error) {
// contract is not implemented yet
return 0, nil
}
Loading

0 comments on commit 43d8c54

Please sign in to comment.