Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[payments] disperser server metering #792

Merged
merged 18 commits into from
Oct 30, 2024
Merged
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
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
Loading