From c613cdd0478e0915a201c7d9ec545659a0e25013 Mon Sep 17 00:00:00 2001 From: hopeyen Date: Wed, 23 Oct 2024 09:53:44 -0700 Subject: [PATCH] feat: payment signing and verifying --- core/auth.go | 6 +++ core/auth/payment_signer.go | 79 ++++++++++++++++++++++++++++++++ core/auth/payment_signer_test.go | 78 +++++++++++++++++++++++++++++++ core/data.go | 20 +++++--- disperser/apiserver/server.go | 11 +++-- 5 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 core/auth/payment_signer.go create mode 100644 core/auth/payment_signer_test.go diff --git a/core/auth.go b/core/auth.go index 1348be4c9..ec576c1f1 100644 --- a/core/auth.go +++ b/core/auth.go @@ -1,5 +1,7 @@ package core +import commonpb "github.com/Layr-Labs/eigenda/api/grpc/common" + type BlobRequestAuthenticator interface { AuthenticateBlobRequest(header BlobAuthHeader) error } @@ -8,3 +10,7 @@ type BlobRequestSigner interface { SignBlobRequest(header BlobAuthHeader) ([]byte, error) GetAccountID() (string, error) } + +type PaymentSigner interface { + SignBlobPayment(header *commonpb.PaymentHeader) ([]byte, error) +} diff --git a/core/auth/payment_signer.go b/core/auth/payment_signer.go new file mode 100644 index 000000000..b4e0dce80 --- /dev/null +++ b/core/auth/payment_signer.go @@ -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 + ) +} diff --git a/core/auth/payment_signer_test.go b/core/auth/payment_signer_test.go new file mode 100644 index 000000000..2d513a1ee --- /dev/null +++ b/core/auth/payment_signer_test.go @@ -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") + }) +} diff --git a/core/data.go b/core/data.go index 21e24330c..c10e8a93f 100644 --- a/core/data.go +++ b/core/data.go @@ -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" ) @@ -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, diff --git a/disperser/apiserver/server.go b/disperser/apiserver/server.go index dd15d8920..88eb542f5 100644 --- a/disperser/apiserver/server.go +++ b/disperser/apiserver/server.go @@ -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 {