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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions core/auth.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package core

import commonpb "github.com/Layr-Labs/eigenda/api/grpc/common"

type BlobRequestAuthenticator interface {
AuthenticateBlobRequest(header BlobAuthHeader) error
}
Expand All @@ -8,3 +10,8 @@ type BlobRequestSigner interface {
SignBlobRequest(header BlobAuthHeader) ([]byte, error)
GetAccountID() (string, error)
}

type PaymentSigner interface {
SignBlobPayment(header *commonpb.PaymentHeader) ([]byte, error)
GetAccountID() string
}
92 changes: 92 additions & 0 deletions core/auth/payment_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 := pm.Hash()

sig, err := crypto.Sign(hash.Bytes(), 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 := pm.Hash()

recoveredPubKey, err := crypto.SigToPub(hash.Bytes(), 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.Bytes(),
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")
})
}
20 changes: 13 additions & 7 deletions core/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
"fmt"
"math/big"

commonpb "github.com/Layr-Labs/eigenda/api/grpc/common"
"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/encoding"
"github.com/consensys/gnark-crypto/ecc/bn254"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

Expand Down Expand Up @@ -494,23 +496,27 @@ type PaymentMetadata struct {
}

// Hash returns the Keccak256 hash of the PaymentMetadata
func (pm *PaymentMetadata) Hash() []byte {
// Create a byte slice to hold the serialized data
func (pm *PaymentMetadata) Hash() gethcommon.Hash {
data := make([]byte, 0, len(pm.AccountID)+4+pm.CumulativePayment.BitLen()/8+1)

// Append AccountID
data = append(data, []byte(pm.AccountID)...)

// Append BinIndex
binIndexBytes := make([]byte, 4)
binary.BigEndian.PutUint32(binIndexBytes, pm.BinIndex)
data = append(data, binIndexBytes...)

// Append CumulativePayment
paymentBytes := pm.CumulativePayment.Bytes()
data = append(data, paymentBytes...)

return crypto.Keccak256(data)
return crypto.Keccak256Hash(data)
}

// 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,
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