Skip to content
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
8 changes: 5 additions & 3 deletions api/clients/v2/disperser_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import (
"sync"
"time"

"github.com/docker/go-units"

"github.com/Layr-Labs/eigenda/api"
disperser_rpc "github.com/Layr-Labs/eigenda/api/grpc/disperser/v2"
"github.com/Layr-Labs/eigenda/core"
corev2 "github.com/Layr-Labs/eigenda/core/v2"
dispv2 "github.com/Layr-Labs/eigenda/disperser/common/v2"
"github.com/Layr-Labs/eigenda/encoding"
"github.com/Layr-Labs/eigenda/encoding/rs"
"github.com/docker/go-units"
"google.golang.org/grpc"
)

Expand Down Expand Up @@ -297,14 +296,17 @@ func (c *disperserClient) GetPaymentState(ctx context.Context) (*disperser_rpc.G
return nil, fmt.Errorf("error getting signer's account ID: %w", err)
}

signature, err := c.signer.SignPaymentStateRequest()
timestamp := uint64(time.Now().UnixNano())

signature, err := c.signer.SignPaymentStateRequest(timestamp)
if err != nil {
return nil, fmt.Errorf("error signing payment state request: %w", err)
}

request := &disperser_rpc.GetPaymentStateRequest{
AccountId: accountID.Hex(),
Signature: signature,
Timestamp: timestamp,
}
return c.client.GetPaymentState(ctx, request)
}
Expand Down
8 changes: 8 additions & 0 deletions api/docs/disperser_v2.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions api/docs/eigenda-protos.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

258 changes: 135 additions & 123 deletions api/grpc/disperser/v2/disperser_v2.pb.go

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions api/hashing/payment_state_hashing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package hashing

import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"golang.org/x/crypto/sha3"
)

// HashGetPaymentStateRequest hashes the given GetPaymentStateRequest from accountId and timestamp
func HashGetPaymentStateRequest(accountId common.Address, timestamp uint64) ([]byte, error) {
hasher := sha3.NewLegacyKeccak256()

// Hash the accountId
err := hashByteArray(hasher, accountId.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to hash account id: %w", err)
}

// Hash the timestamp
hashUint64(hasher, timestamp)

return hasher.Sum(nil), nil
}
3 changes: 3 additions & 0 deletions api/proto/disperser/v2/disperser_v2.proto
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ message GetPaymentStateRequest {
string account_id = 1;
// Signature over the account ID
bytes signature = 2;
// Timestamp of the request in nanoseconds since the Unix epoch. If too far out of sync with the server's clock,
// request may be rejected.
uint64 timestamp = 3;
}

// GetPaymentStateReply contains the payment state of an account.
Expand Down
65 changes: 45 additions & 20 deletions core/auth/v2/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package v2_test

import (
"crypto/sha256"
disperser_rpc "github.com/Layr-Labs/eigenda/api/grpc/disperser/v2"
"github.com/Layr-Labs/eigenda/api/hashing"
"github.com/Layr-Labs/eigenda/common/replay"
"math/big"
"testing"
"time"

"github.com/Layr-Labs/eigenda/core"
auth "github.com/Layr-Labs/eigenda/core/auth/v2"
Expand All @@ -16,13 +20,16 @@ import (
)

var (
privateKeyHex = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
privateKeyHex = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
maxPastAge = 5 * time.Minute
maxFutureAge = 5 * time.Minute
fixedTimestamp = uint64(1609459200000000000)
)

func TestAuthentication(t *testing.T) {
signer, err := auth.NewLocalBlobRequestSigner(privateKeyHex)
assert.NoError(t, err)
authenticator := auth.NewAuthenticator()
blobRequestAuthenticator := auth.NewBlobRequestAuthenticator()

accountId, err := signer.GetAccountID()
assert.NoError(t, err)
Expand All @@ -32,15 +39,14 @@ func TestAuthentication(t *testing.T) {
signature, err := signer.SignBlobRequest(header)
assert.NoError(t, err)

err = authenticator.AuthenticateBlobRequest(header, signature)
err = blobRequestAuthenticator.AuthenticateBlobRequest(header, signature)
assert.NoError(t, err)

}

func TestAuthenticationFail(t *testing.T) {
signer, err := auth.NewLocalBlobRequestSigner(privateKeyHex)
assert.NoError(t, err)
authenticator := auth.NewAuthenticator()
blobRequestAuthenticator := auth.NewBlobRequestAuthenticator()

accountId, err := signer.GetAccountID()
assert.NoError(t, err)
Expand All @@ -55,7 +61,7 @@ func TestAuthenticationFail(t *testing.T) {
signature, err := signer.SignBlobRequest(header)
assert.NoError(t, err)

err = authenticator.AuthenticateBlobRequest(header, signature)
err = blobRequestAuthenticator.AuthenticateBlobRequest(header, signature)
assert.Error(t, err)
}

Expand Down Expand Up @@ -119,38 +125,43 @@ func testHeader(t *testing.T, accountID gethcommon.Address) *corev2.BlobHeader {
func TestAuthenticatePaymentStateRequestValid(t *testing.T) {
signer, err := auth.NewLocalBlobRequestSigner(privateKeyHex)
assert.NoError(t, err)
authenticator := auth.NewAuthenticator()
paymentStateAuthenticator := auth.NewPaymentStateAuthenticator(maxPastAge, maxFutureAge)
paymentStateAuthenticator.ReplayGuardian = replay.NewNoOpReplayGuardian()

signature, err := signer.SignPaymentStateRequest()
signature, err := signer.SignPaymentStateRequest(fixedTimestamp)
assert.NoError(t, err)
assert.NotNil(t, signature)

accountId, err := signer.GetAccountID()
assert.NoError(t, err)

err = authenticator.AuthenticatePaymentStateRequest(signature, accountId)
request := mockGetPaymentStateRequest(accountId, signature)

err = paymentStateAuthenticator.AuthenticatePaymentStateRequest(accountId, request)
assert.NoError(t, err)
}

func TestAuthenticatePaymentStateRequestInvalidSignatureLength(t *testing.T) {
authenticator := auth.NewAuthenticator()
paymentStateAuthenticator := auth.NewPaymentStateAuthenticator(maxPastAge, maxFutureAge)
request := mockGetPaymentStateRequest(gethcommon.HexToAddress("0x123"), []byte{1, 2, 3})

err := authenticator.AuthenticatePaymentStateRequest([]byte{1, 2, 3}, gethcommon.HexToAddress("0x123"))
err := paymentStateAuthenticator.AuthenticatePaymentStateRequest(gethcommon.HexToAddress("0x123"), request)
assert.Error(t, err)
assert.Contains(t, err.Error(), "signature length is unexpected")
}

func TestAuthenticatePaymentStateRequestInvalidPublicKey(t *testing.T) {
authenticator := auth.NewAuthenticator()

err := authenticator.AuthenticatePaymentStateRequest(make([]byte, 65), gethcommon.Address{})
paymentStateAuthenticator := auth.NewPaymentStateAuthenticator(maxPastAge, maxFutureAge)
request := mockGetPaymentStateRequest(gethcommon.Address{}, make([]byte, 65))
err := paymentStateAuthenticator.AuthenticatePaymentStateRequest(gethcommon.Address{}, request)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to recover public key from signature")
}

func TestAuthenticatePaymentStateRequestSignatureMismatch(t *testing.T) {
signer, err := auth.NewLocalBlobRequestSigner(privateKeyHex)
assert.NoError(t, err)
authenticator := auth.NewAuthenticator()
paymentStateAuthenticator := auth.NewPaymentStateAuthenticator(maxPastAge, maxFutureAge)

// Create a different signer with wrong private key
wrongSigner, err := auth.NewLocalBlobRequestSigner("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcded")
Expand All @@ -160,29 +171,43 @@ func TestAuthenticatePaymentStateRequestSignatureMismatch(t *testing.T) {
accountId, err := signer.GetAccountID()
assert.NoError(t, err)

signature, err := wrongSigner.SignPaymentStateRequest()
signature, err := wrongSigner.SignPaymentStateRequest(uint64(time.Now().UnixNano()))
assert.NoError(t, err)

err = authenticator.AuthenticatePaymentStateRequest(signature, accountId)
request := mockGetPaymentStateRequest(accountId, signature)

err = paymentStateAuthenticator.AuthenticatePaymentStateRequest(accountId, request)
assert.Error(t, err)
assert.Contains(t, err.Error(), "signature doesn't match with provided public key")
}

func TestAuthenticatePaymentStateRequestCorruptedSignature(t *testing.T) {
signer, err := auth.NewLocalBlobRequestSigner(privateKeyHex)
assert.NoError(t, err)
authenticator := auth.NewAuthenticator()
paymentStateAuthenticator := auth.NewPaymentStateAuthenticator(maxPastAge, maxFutureAge)

accountId, err := signer.GetAccountID()
assert.NoError(t, err)

hash := sha256.Sum256(accountId.Bytes())
requestHash, err := hashing.HashGetPaymentStateRequest(accountId, fixedTimestamp)
assert.NoError(t, err)

hash := sha256.Sum256(requestHash)
signature, err := crypto.Sign(hash[:], signer.PrivateKey)
assert.NoError(t, err)

// Corrupt the signature
signature[0] ^= 0x01
request := mockGetPaymentStateRequest(accountId, signature)

err = authenticator.AuthenticatePaymentStateRequest(signature, accountId)
err = paymentStateAuthenticator.AuthenticatePaymentStateRequest(accountId, request)
assert.Error(t, err)
}

func mockGetPaymentStateRequest(accountId gethcommon.Address, signature []byte) *disperser_rpc.GetPaymentStateRequest {
return &disperser_rpc.GetPaymentStateRequest{
AccountId: accountId.Hex(),
Signature: signature,
Timestamp: fixedTimestamp,
}
}
45 changes: 39 additions & 6 deletions core/auth/v2/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,34 @@ import (
"crypto/sha256"
"errors"
"fmt"
pb "github.com/Layr-Labs/eigenda/api/grpc/disperser/v2"
"github.com/Layr-Labs/eigenda/api/hashing"
"time"

"github.com/Layr-Labs/eigenda/common/replay"
core "github.com/Layr-Labs/eigenda/core/v2"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

type authenticator struct{}
type authenticator struct {
ReplayGuardian replay.ReplayGuardian
}

// NewBlobRequestAuthenticator creates an authenticator for blob requests.
// ReplayGuardian is not used for blob requests.
func NewBlobRequestAuthenticator() *authenticator {
return &authenticator{
ReplayGuardian: nil, // Not needed for blob requests
}
}

func NewAuthenticator() *authenticator {
return &authenticator{}
// NewPaymentStateAuthenticator creates an authenticator for payment state requests,
// which requires replay protection.
func NewPaymentStateAuthenticator(maxTimeInPast, maxTimeInFuture time.Duration) *authenticator {
return &authenticator{
ReplayGuardian: replay.NewReplayGuardian(time.Now, maxTimeInPast, maxTimeInFuture),
}
}

var _ core.BlobRequestAuthenticator = &authenticator{}
Expand Down Expand Up @@ -46,16 +64,22 @@ func (*authenticator) AuthenticateBlobRequest(header *core.BlobHeader, signature
}

// AuthenticatePaymentStateRequest verifies the signature of the payment state request
// The signature is signed over the byte representation of the account ID
// The signature is signed over the byte representation of the account ID and requestHash
// See implementation of BlobRequestSigner.SignPaymentStateRequest for more details
func (*authenticator) AuthenticatePaymentStateRequest(sig []byte, accountAddr common.Address) error {
func (a *authenticator) AuthenticatePaymentStateRequest(accountAddr common.Address, request *pb.GetPaymentStateRequest) error {
sig := request.GetSignature()
// Ensure the signature is 65 bytes (Recovery ID is the last byte)
if len(sig) != 65 {
return fmt.Errorf("signature length is unexpected: %d", len(sig))
}

requestHash, err := hashing.HashGetPaymentStateRequest(accountAddr, request.GetTimestamp())
if err != nil {
return fmt.Errorf("failed to hash request: %w", err)
}
hash := sha256.Sum256(requestHash)

// Verify the signature
hash := sha256.Sum256(accountAddr.Bytes())
sigPublicKeyECDSA, err := crypto.SigToPub(hash[:], sig)
if err != nil {
return fmt.Errorf("failed to recover public key from signature: %v", err)
Expand All @@ -67,5 +91,14 @@ func (*authenticator) AuthenticatePaymentStateRequest(sig []byte, accountAddr co
return errors.New("signature doesn't match with provided public key")
}

if a.ReplayGuardian == nil {
return errors.New("replay guardian is not configured for payment state requests")
}

timestamp := request.GetTimestamp()
if err := a.ReplayGuardian.VerifyRequest(requestHash, time.Unix(0, int64(timestamp))); err != nil {
return fmt.Errorf("failed to verify request: %v", err)
}

return nil
}
12 changes: 9 additions & 3 deletions core/auth/v2/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/ecdsa"
"crypto/sha256"
"fmt"
"github.com/Layr-Labs/eigenda/api/hashing"

core "github.com/Layr-Labs/eigenda/core/v2"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -44,13 +45,18 @@ func (s *LocalBlobRequestSigner) SignBlobRequest(header *core.BlobHeader) ([]byt
return sig, nil
}

func (s *LocalBlobRequestSigner) SignPaymentStateRequest() ([]byte, error) {
func (s *LocalBlobRequestSigner) SignPaymentStateRequest(timestamp uint64) ([]byte, error) {
accountId, err := s.GetAccountID()
if err != nil {
return nil, fmt.Errorf("failed to get account ID: %v", err)
}

hash := sha256.Sum256(accountId.Bytes())
requestHash, err := hashing.HashGetPaymentStateRequest(accountId, timestamp)
if err != nil {
return nil, fmt.Errorf("failed to hash request: %w", err)
}

hash := sha256.Sum256(requestHash)
// Sign the account ID using the private key
sig, err := crypto.Sign(hash[:], s.PrivateKey)
if err != nil {
Expand All @@ -77,7 +83,7 @@ func (s *LocalNoopSigner) SignBlobRequest(header *core.BlobHeader) ([]byte, erro
return nil, fmt.Errorf("noop signer cannot sign blob request")
}

func (s *LocalNoopSigner) SignPaymentStateRequest() ([]byte, error) {
func (s *LocalNoopSigner) SignPaymentStateRequest(timestamp uint64) ([]byte, error) {
return nil, fmt.Errorf("noop signer cannot sign payment state request")
}

Expand Down
Loading
Loading