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

Relay authentication #911

Merged
merged 38 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
02e9069
Added rate limiting.
cody-littley Nov 15, 2024
dc39c21
Properly handle blob sizes.
cody-littley Nov 15, 2024
d99fd39
Incremental progress.
cody-littley Nov 15, 2024
e39eaf9
Incremental progress.
cody-littley Nov 15, 2024
876ec69
Merge branch 'master' into relay-rate-limits2
cody-littley Nov 18, 2024
9500f4f
unit tests
cody-littley Nov 18, 2024
ffe18c6
Unit tests.
cody-littley Nov 18, 2024
79d9614
Fix tests.
cody-littley Nov 18, 2024
c1d83e6
Cleanup.
cody-littley Nov 18, 2024
ed4daca
Added get chunks request hashing.
cody-littley Nov 18, 2024
3438b92
Start work on authenticator.
cody-littley Nov 18, 2024
b5dc37c
Fix test issue.
cody-littley Nov 18, 2024
b420cca
Cleanup
cody-littley Nov 18, 2024
517b78e
Merge branch 'master' into relay-rate-limits2
cody-littley Nov 19, 2024
7982aec
Convert config to flag pattern.
cody-littley Nov 19, 2024
366fdf7
Simplify rate limiter classes.
cody-littley Nov 19, 2024
f2c10e4
Made suggested changes.
cody-littley Nov 19, 2024
0f95ce4
Merge branch 'relay-rate-limits2' into relay-authentication
cody-littley Nov 19, 2024
c2bb9c3
Shorten package name.
cody-littley Nov 19, 2024
151305b
Started testing
cody-littley Nov 19, 2024
29fd940
Finished unit tests.
cody-littley Nov 19, 2024
3993af4
Nil authenticator test.
cody-littley Nov 19, 2024
d094b80
Test with authentication saving disabled.
cody-littley Nov 19, 2024
d8691e1
Tie together config.
cody-littley Nov 19, 2024
8b9512b
Add method for convenient signing.
cody-littley Nov 19, 2024
fb7ec51
Made requested changes.
cody-littley Nov 19, 2024
6325f0c
Merge branch 'master' into relay-authentication
cody-littley Nov 19, 2024
5f853e0
Revert unintentional changes.
cody-littley Nov 19, 2024
f22544c
Fix bug.
cody-littley Nov 20, 2024
7a26c27
Made requested changes.
cody-littley Nov 20, 2024
d21e0bc
Update proto documentation.
cody-littley Nov 20, 2024
98a8469
Add key caching.
cody-littley Nov 20, 2024
5948e73
lint
cody-littley Nov 20, 2024
cc62eac
Make requested changes.
cody-littley Nov 20, 2024
0372ee9
Made suggested changes.
cody-littley Nov 20, 2024
97cc318
Made requested changes.
cody-littley Nov 20, 2024
fea251a
Add preloading of keys.
cody-littley Nov 20, 2024
fe82924
Made suggested changes.
cody-littley Nov 21, 2024
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: 4 additions & 4 deletions api/grpc/relay/relay.pb.go

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

2 changes: 1 addition & 1 deletion api/proto/relay/relay.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ message GetChunksRequest {

// If this is an authenticated request, this should hold the ID of the requester. If this
// is an unauthenticated request, this field should be empty.
uint64 requester_id = 2;
bytes requester_id = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the code below, this is interpreted as operator ID. It'll be helpful to document it here so the users know how to set it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed this field to operator_id. Is that sufficient, or do you think I should add additional documentation?


// If this is an authenticated request, this field will hold a signature by the requester
// on the chunks being requested.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document how this signature should be computed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  // If this is an authenticated request, this field will hold a BLS signature by the requester
  // on the hash of this request. Relays may choose to reject unauthenticated requests.
  //
  // The following describes the schema for computing the hash of this request
  // This algorithm is implemented in golang using relay.auth.HashGetChunksRequest().
  //
  // All integers are encoded as unsigned 4 byte big endian values.
  //
  // Perform a keccak256 hash on the following data in the following order:
  // 1. the operator id
  // 2. for each chunk request:
  //    a. if the chunk request is a request by index:
  //       i. the blob key
  //       ii. the start index
  //       iii. the end index
  //    b. if the chunk request is a request by range:
  //       i. the blob key
  //       ii. each requested chunk index, in order
  bytes operator_signature = 3;

Expand Down
159 changes: 159 additions & 0 deletions relay/auth/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package auth

import (
"context"
"errors"
"fmt"
pb "github.com/Layr-Labs/eigenda/api/grpc/relay"
"github.com/Layr-Labs/eigenda/core"
"sync"
"time"
)

// RequestAuthenticator authenticates requests to the relay service. This object is thread safe.
type RequestAuthenticator interface {
// AuthenticateGetChunksRequest authenticates a GetChunksRequest, returning an error if the request is invalid.
// The address is the address of the peer that sent the request. This may be used to cache auth results
// in order to save server resources.
AuthenticateGetChunksRequest(
address string,
request *pb.GetChunksRequest,
now time.Time) error
}

// authenticationTimeout is used to track the expiration of an auth.
type authenticationTimeout struct {
clientID string
expiration time.Time
}

var _ RequestAuthenticator = &requestAuthenticator{}

type requestAuthenticator struct {
ics core.IndexedChainState

// authenticatedClients is a set of client IDs that have been recently authenticated.
authenticatedClients map[string]struct{}

// authenticationTimeouts is a list of authentications that have been performed, along with their expiration times.
authenticationTimeouts []*authenticationTimeout

// authenticationTimeoutDuration is the duration for which an auth is valid.
// If this is zero, then auth saving is disabled, and each request will be authenticated independently.
authenticationTimeoutDuration time.Duration

// savedAuthLock is used for thread safe atomic modification of the authenticatedClients map and the
// authenticationTimeouts queue.
savedAuthLock sync.Mutex
}

// NewRequestAuthenticator creates a new RequestAuthenticator.
func NewRequestAuthenticator(
ics core.IndexedChainState,
authenticationTimeoutDuration time.Duration) RequestAuthenticator {

return &requestAuthenticator{
ics: ics,
authenticatedClients: make(map[string]struct{}),
authenticationTimeouts: make([]*authenticationTimeout, 0),
authenticationTimeoutDuration: authenticationTimeoutDuration,
}
}

func (a *requestAuthenticator) AuthenticateGetChunksRequest(
address string,
ian-shim marked this conversation as resolved.
Show resolved Hide resolved
request *pb.GetChunksRequest,
now time.Time) error {

if a == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this compare to check the authenticator from outside?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I have it here is wrong (it didn't pass unit tests). I removed this check and instead now make it in the outside context.

// do not enforce auth if the authenticator is nil
return nil
}

if a.isAuthenticationStillValid(now, address) {
// We've recently authenticated this client. Do not authenticate again for a while.
return nil
}

blockNumber, err := a.ics.GetCurrentBlockNumber()
if err != nil {
return fmt.Errorf("failed to get current block number: %w", err)
}
operators, err := a.ics.GetIndexedOperators(context.Background(), blockNumber)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 RPC calls here, it'd be helpful to cache the operator state

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

caching added

if err != nil {
return fmt.Errorf("failed to get operators: %w", err)
}

operatorID := core.OperatorID(request.RequesterId)
operator, ok := operators[operatorID]
if !ok {
return errors.New("operator not found")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be helpful to include the block number in error message

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

}
key := operator.PubkeyG2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd cache operatorID->G2, this is a mapping that'll not change (hence highly valuable to cache)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. This will break if we ever allow operators to change keys, but that's a bridge we can cross in the future.


g1Point, err := (&core.G1Point{}).Deserialize(request.RequesterSignature)
if err != nil {
return fmt.Errorf("failed to deserialize signature: %w", err)
}

signature := core.Signature{
G1Point: g1Point,
}

hash := HashGetChunksRequest(request)
isValid := signature.Verify(key, ([32]byte)(hash))

if !isValid {
return errors.New("signature verification failed")
}

a.saveAuthenticationResult(now, address)
return nil
}

// saveAuthenticationResult saves the result of an auth.
func (a *requestAuthenticator) saveAuthenticationResult(now time.Time, address string) {
if a.authenticationTimeoutDuration == 0 {
// Authentication saving is disabled.
return
}

a.savedAuthLock.Lock()
defer a.savedAuthLock.Unlock()

a.authenticatedClients[address] = struct{}{}
a.authenticationTimeouts = append(a.authenticationTimeouts,
&authenticationTimeout{
clientID: address,
expiration: now.Add(a.authenticationTimeoutDuration),
})
}

// isAuthenticationStillValid returns true if the client at the given address has been authenticated recently.
func (a *requestAuthenticator) isAuthenticationStillValid(now time.Time, address string) bool {
if a.authenticationTimeoutDuration == 0 {
// Authentication saving is disabled.
return false
}

a.savedAuthLock.Lock()
defer a.savedAuthLock.Unlock()

a.removeOldAuthentications(now)
_, ok := a.authenticatedClients[address]
return ok
}

// removeOldAuthentications removes any authentications that have expired.
ian-shim marked this conversation as resolved.
Show resolved Hide resolved
func (a *requestAuthenticator) removeOldAuthentications(now time.Time) {
index := 0
for ; index < len(a.authenticationTimeouts); index++ {
if a.authenticationTimeouts[index].expiration.After(now) {
break
}
delete(a.authenticatedClients, a.authenticationTimeouts[index].clientID)
}
if index > 0 {
a.authenticationTimeouts = a.authenticationTimeouts[index:]
}
}
Loading
Loading