Skip to content

Commit

Permalink
feat: payment signing and verifying
Browse files Browse the repository at this point in the history
  • Loading branch information
hopeyen committed Oct 23, 2024
1 parent 03b0475 commit c613cdd
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 10 deletions.
6 changes: 6 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,7 @@ type BlobRequestSigner interface {
SignBlobRequest(header BlobAuthHeader) ([]byte, error)
GetAccountID() (string, error)
}

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

import (
"crypto/ecdsa"
"encoding/hex"
"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,
}
}

func (s *PaymentSigner) SignBlobPayment(header *commonpb.PaymentHeader) ([]byte, error) {
// Set the account id to the hex encoded public key of the signer
header.AccountId = hex.EncodeToString(crypto.FromECDSAPub(&s.PrivateKey.PublicKey))
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")
}

// VerifyPaymentSignature verifies the signature against the payment metadata
func VerifyPaymentSignature(paymentHeader *commonpb.PaymentHeader, paymentSignature []byte) bool {
pubKeyBytes, err := hex.DecodeString(paymentHeader.AccountId)
if err != nil {
log.Printf("Failed to decode AccountId: %v\n", err)
return false
}
accountPubKey, err := crypto.UnmarshalPubkey(pubKeyBytes)
if err != nil {
log.Printf("Failed to unmarshal public key: %v\n", err)
return false
}

pm := core.ConvertPaymentHeader(paymentHeader)
hash := pm.Hash()

return crypto.VerifySignature(
crypto.FromECDSAPub(accountPubKey),
hash.Bytes(),
paymentSignature[:len(paymentSignature)-1], // Remove recovery ID
)
}
78 changes: 78 additions & 0 deletions core/auth/payment_signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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) {
// Generate a new private key for testing
privateKey, err := crypto.GenerateKey()
// publicKey := &privateKey.PublicKey
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)
}

// Hash returns the Keccak256 hash of the 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
11 changes: 8 additions & 3 deletions disperser/apiserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1156,9 +1156,14 @@ func (s *DispersalServer) validatePaidRequestAndGetBlob(ctx context.Context, req
}

seenQuorums := make(map[uint8]struct{})
// The quorum ID must be in range [0, 254]. It'll actually be converted
// to uint8, so it cannot be greater than 254.
// No check with required quorums

// TODO: validate payment signature against payment metadata
if !auth.VerifyPaymentSignature(req.GetPaymentHeader(), req.GetPaymentSignature()) {
return nil, fmt.Errorf("payment signature is invalid")
}
// Unlike regular blob dispersal request validation, there's no check with required quorums
// Because Reservation has their specific quorum requirements, and on-demand is only allowed and paid to the required quorums.
// Payment specific validations are done within the meterer library.
for i := range req.GetCustomQuorumNumbers() {

if req.GetCustomQuorumNumbers()[i] > core.MaxQuorumID {
Expand Down

0 comments on commit c613cdd

Please sign in to comment.