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

Read only mode #126

Closed
wants to merge 3 commits into from
Closed

Read only mode #126

wants to merge 3 commits into from

Conversation

hopeyen
Copy link
Collaborator

@hopeyen hopeyen commented Sep 17, 2024

  • Added flag EIGENDA_PROXY_READ_ONLY
  • Generate random private key if not provided
  • Add readonly config to EigenDA router that simply return bad request when calling PUT methods

Makefile Outdated Show resolved Hide resolved
bxue-l2
bxue-l2 previously approved these changes Sep 17, 2024
Copy link
Collaborator

@bxue-l2 bxue-l2 left a comment

Choose a reason for hiding this comment

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

lgtm

server/config.go Outdated Show resolved Hide resolved
server/config.go Outdated
Comment on lines 513 to 518
&cli.BoolFlag{
Name: ReadOnlyFlagName,
Usage: "Whether the proxy should operate in read-only mode.",
Value: false,
EnvVars: prefixEnvVars("READ_ONLY"),
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are we adding this? Who is this feature useful for? Can we add some description here or somewhere else as to the intent of this feature.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a feature included in the v1.5.0 document. I think this was requested by celo. The use case is they only need retrieval when running proxy, and it made sense to develop a read mode that does not require a key.

I've modified the usage string to say Whether to run the proxy in read-only mode; set to true to only allow retrieval from EigenDA. Please let me know if this is sufficient

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the problem now is that if some op-nodes just want to run the derivation pipeline, there is no way to start the proxy without putting in some fake privateKey.

Comment on lines +122 to +124
if r.readonly {
return nil, errors.New("read-only router does not support Put")
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What will this return to the client? Will the Put handler return a 500 with this error?

One other idea is to just disable the put route/handler entirely when in read mode. Have you considered that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this will return 500 with this error.

I've considered this approach and tried implementing this approach prior to the current PR. There's restriction from IRouter interface requiring the [put](https://github.com/Layr-Labs/eigenda-proxy/blob/d124ef8bd80eb97f4feadd6901cc3947d6350874/store/router.go#L18) handler, but I didn't think it made sense to remove put from the interface?

Also thought about making a separate router type that contains exactly this super simple put handler, and create Router type based on the readonly config, but there was a lot of code repetition for all the other functions, and I didn't find a way to easily share functions between two child struct types (the regular Router and a ReadOnly 🤔 And thought about a wrapper type, but didn't think it was worth it at the end and a bit counter-intuitive .

Please suggest and I'm happy to learn and redo:)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Gotcha! This works I think. We should be returning a 400 instead though. I noticed we return 500s in a bunch of places where we shouldn't (see #127 for eg)

@bxue-l2 bxue-l2 self-requested a review September 17, 2024 20:53
server/config.go Outdated
DisableTLS: ctx.Bool(DisableTLSFlagName),
ResponseTimeout: ctx.Duration(ResponseTimeoutFlagName),
CustomQuorumIDs: ctx.UintSlice(CustomQuorumIDsFlagName),
SignerPrivateKeyHex: func() string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

actually, if someone forgot to set the key, and does not enable read only. Then the server can send unauthenticated data

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I realized this as well, I was able to request for dispersal even with random private keys. This was confusing because I thought disperser had authentication checks with ratelimits

Copy link
Collaborator

Choose a reason for hiding this comment

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

disperser has a free tier for testnet. but it does not have a free tier for mainnet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the server won't be able to send unauthenticated data on the mainnet due to rate-limits, right? what's the desirable behavior in this case? (I'm going to update this random generation to use a constant address like all zeros)

@bxue-l2 bxue-l2 dismissed their stale review September 17, 2024 21:05

see the new comments

Comment on lines +168 to +177
signerPrivateKeyHex := func() string {
if key := ctx.String(SignerPrivateKeyHexFlagName); key != "" {
return key
}
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return ""
}
return hex.EncodeToString(randomBytes)
}()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
signerPrivateKeyHex := func() string {
if key := ctx.String(SignerPrivateKeyHexFlagName); key != "" {
return key
}
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return ""
}
return hex.EncodeToString(randomBytes)
}()
signerPrivateKeyHex := ctx.String(SignerPrivateKeyHexFlagName)
if signerPrivateKeyHex == "" {
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return ""
}
signerPrivateKeyHex = hex.EncodeToString(randomBytes)
}

Can we do something simpler like this? Really don't like inline functions when not needed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also not sure about returning "" when rand fails... maybe panic was right. Not super sure what the intended behavior is here so I'll leave that to you.

Comment on lines +168 to +177
signerPrivateKeyHex := func() string {
if key := ctx.String(SignerPrivateKeyHexFlagName); key != "" {
return key
}
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return ""
}
return hex.EncodeToString(randomBytes)
}()
Copy link
Collaborator

Choose a reason for hiding this comment

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

how do we handle the case in-which read mode is disable and the user didn't provide a valid key? should we fail loudly assuming this could be an error case that someone running in production would need to know about?

Copy link
Collaborator

Choose a reason for hiding this comment

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

knit - would prefer this to live as a separate function within this file to avoid overlaying additional programming abstractions (i.e, callbacks) that aren't generally adopted within the service

Comment on lines +122 to +124
if r.readonly {
return nil, errors.New("read-only router does not support Put")
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

what if I just wanna run proxy for S3 support using the keccak256 backend and would like to disable reading from eigenda?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm a bit confused, the read-only mode implemented here will disable all put methods no matter what. Are you saying the read-only mode should handle something more granular with the cache/fallback targets?

@@ -87,3 +87,6 @@ EIGENDA_PROXY_SERVICE_MANAGER_ADDR=0xD4A7E1Bd8015057293f0D0A557088c286942e84b

# endpoint for S3 storage
# EIGENDA_PROXY_S3_ENDPOINT

# set to true to disable writing to EigenDA
# EIGENDA_PROXY_READ_ONLY=false
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking about this a bit more pragmatically - we have a LOT of knobs for this service with the env namespace only growing. Would it be better to process this internally using the SignerPrivateKey where we treat it as readonly <-> SignerPrivateKey="" or unset by the user?

A lil hesitant to introduce more env flags, thoughts?
cc - @samlaf, @bxue-l2

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah I certainly don't like the fact that we have growing flags without obvious links between themselves. I don't really like the privateKey being empty turning readonly mode though, as that is very surprising. One thing we could do eventually is move to urfave v3 and use mutuallyExclusiveFlags (https://pkg.go.dev/github.com/urfave/cli/v3#MutuallyExclusiveFlags). So basically urfave would force user to either be in readonly mode, or to provide a signerPrivateKey, but can't have both.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Regarding the UX problem of celo wanting to run in read-only mode and not having to provide a random signerPrivateKey, seems like the main source of the problem is in the eigenda-client? https://github.com/Layr-Labs/eigenda/blob/97dd2a3da09fd62ff3670afd96c2d402f1ca8453/api/clients/config.go#L55 seems like having a non-empty signerPrivateKey is enforced? Perhaps we would need a read-only eigenda-client?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I agree with all the concerns here. I would suggest having a separate refactor effort for the flags.

For comment on eigenda-client, I thought so too, but didn't want to touch eigenda code 😬 maybe I should've taken the initiative there which makes more sense. I can go ahead and put a PR there and see what corresponding changes would be good here

Copy link
Collaborator Author

@hopeyen hopeyen Sep 18, 2024

Choose a reason for hiding this comment

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

reevaluating the approach. Currently EigenDAClient only contains a dispersal client that has a separate definition for RetrieveBlob than the one for retrieval client that has a more complex/decentralized retrieval logic.

thinking about read-only's UX, perhaps we want to simply create an RetrievalClient instead of an EigenDAClient and let retrieveBlob serve Get and return error for Put. But then, why don't the read-only user simply spin up a retrieval client? lmk what you guys think, can also bring this up in the sync

Copy link
Collaborator

Choose a reason for hiding this comment

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

We didn't have time to discuss that in the sync. In the current EigenDAClient, it looks we didn't put much consideration for clients that require read-only (like op-node or nitro node which only runs derivation). Instead of creating patch here and there, it is good to start from a high level design first, which I think Hope is pointing out.

On spinning up a retrieval client. In OP, the protocol requires there to be a proxy server, so the user can't spin up a client, the proxy has to.

On refactor, we could create a client struct that wrap around both node and disperser client. What do people think

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks Bowen, I think taking a step back and agree on high level design makes sense. I think the current PR might be a short term temporary solution if we are okay with this, but I guess no one is rushing to use this, so I'm okay to close this.

For the client struct, are you saying this client struct would live in eigenda or in eigenda-proxy? what's the reason to wrap around a node for a read only mode 🤔? what about waiting for contract storage changes for operator set and implement distributed retrieval for the retriever first?

Copy link
Collaborator

Choose a reason for hiding this comment

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

ignore my suggestion, it is probably too low level of a suggestion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

okay, sorry if my questions were overwhelming. I'm more than happy to learn and understand what you think is best

Comment on lines +97 to 106
// load signer key from environment or generate a random one
pk := os.Getenv(privateKey)
if pk == "" && !testCfg.UseMemory {
t.Fatal("SIGNER_PRIVATE_KEY environment variable not set")
t.Logf("SIGNER_PRIVATE_KEY environment variable not set. Generated random private key")
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
t.Fatalf("Failed to generate random private key: %v", err)
}
pk = hex.EncodeToString(randomBytes)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does generating a unique key do? Looking at the disperser server implementation, rate limits are only enforced when dispersing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also authorization headers are only signed by the client when dispersing as well

Copy link
Collaborator

Choose a reason for hiding this comment

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

All this to say - why can't the key just be a hard-set constant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't want to hardcode a private key constant, but I guess the key could be all zeros for all the read-only mode proxy cares. I will remove the random generation and add a readonly mode conditional here for setting a constant key

Comment on lines +515 to +520
&cli.BoolFlag{
Name: ReadOnlyFlagName,
Usage: "Whether to run the proxy in read-only mode; set to true to only allow retrieval from EigenDA.",
Value: false,
EnvVars: prefixEnvVars("READ_ONLY"),
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's also update the readme documentation so users know how to target this functionality

@hopeyen
Copy link
Collaborator Author

hopeyen commented Sep 20, 2024

gonna close for now and create a separate one after updating eigenda-core's EigenDAClient

@hopeyen hopeyen closed this Sep 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants