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

Skeleton and basic tests for confidential store engine #25

Merged
merged 30 commits into from
Sep 28, 2023

Conversation

Ruteri
Copy link
Collaborator

@Ruteri Ruteri commented Sep 6, 2023

📝 Summary

Readme diff: https://github.com/flashbots/suave-geth/pull/25/files?short_path=b335630#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5

  • Adds a new structure - ConfidentialStoreEngine which manages confidential store interfaces (storage and synchronization).
  • Adds a simple redis (and miniredis) implementation of confidential data store and synchronization protocol. Each Store is now propagated through the transport to other nodes.
  • Adds a message signer based on AccountManager - AccountManagerDASigner. This signer signs messages exchanged through confidential store transport layer.

Changes to the Bid:

  • Changes Id of the Bid structure to be uuidv5 (SHA-1) of the bid
  • Adds Salt to the Bid structure, a random uuid
  • Adds AllowedStores to the Bid structure, an allowlist for which execution nodes are able to modify the bid's data in confidential store

Other notable changes:

  • Refactors how suave stuff is handled in the interpreter - introduces SuaveContext in place of the previous SuaveExecutionBackend. Removes SetConfidentialInputs function.
  • Simplifies ConfidentialStoreBackends, moves all of the validation logic to the new ConfidentialStoreEngine

📚 References


  • I have seen and agree to CONTRIBUTING.md

@Ruteri Ruteri force-pushed the confidential-store-engine branch from 3e2ecc7 to 7144245 Compare September 12, 2023 08:14
@Ruteri Ruteri force-pushed the confidential-store-engine branch from 3d12d74 to 09195e1 Compare September 15, 2023 08:13
@Ruteri Ruteri marked this pull request as ready for review September 15, 2023 12:04
@@ -516,6 +516,18 @@ var (
Category: flags.SuaveCategory,
}

SuaveConfidentialPubsubRedisEndpointFlag = &cli.StringFlag{
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the difference between these two flags? Could they be merged?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They can't be merged - one is the backed for storing data (redis owned by whoever is running the node), and the other is a transport layer - owned by whoever is trusted to run the pubsub. Those are two different redis instances, the transport one is shared among multiple nodes

@@ -0,0 +1,20 @@
# Build in a stock Go builder container
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we running suavecli as a docker container now?

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'll remove the whole docker setup for now and we should think about how to best re-introduce it later on

mrStore2 := miniredis.RunT(t)
mrPubSub := mrStore1

fr1 := newFramework(t, WithExecutionNode(), WithRedisStoreBackend(mrStore1.Addr()), WithRedisStoreTransport(mrPubSub.Addr()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it be possible to do something like this:

Suggested change
fr1 := newFramework(t, WithExecutionNode(), WithRedisStoreBackend(mrStore1.Addr()), WithRedisStoreTransport(mrPubSub.Addr()))
fr1 := newFramework(t, WithExecutionNode(), WithRedisConfidentialStore())

The Framework is intended to provide a high-level abstraction about the deployment of the chain. That includes any setup required.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, yeah that's true

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 changed the store backend to work how you proposed, but the same thing won't quite work for the transport, as it requires a shared redis. I made a helper function that spins up a miniredis instance and returns an opt that is then passed to both frameworks - see if you like it better this way.

ch := make(chan suave.DAMessage, 16)

go func() {
for r.ctx.Err() == nil && ctx.Err() == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

if one of these errors is not nil, would it get logged somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

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

generally on this function, i think we might need to add reconnect logic in case the redis connection drops 🤔

Copy link
Collaborator Author

@Ruteri Ruteri Sep 18, 2023

Choose a reason for hiding this comment

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

Adjusted how context is handled

@@ -25,14 +25,14 @@ contract AnyBidContract {

contract BundleBidContract is AnyBidContract {

function newBid(uint64 decryptionCondition, address[] memory bidAllowedPeekers) external payable returns (bytes memory) {
function newBid(uint64 decryptionCondition, address[] memory bidAllowedPeekers, address[] memory bidAllowedStores) external payable returns (bytes memory) {
Copy link
Member

Choose a reason for hiding this comment

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

can't we treat bidAllowedStores similar to executionNodes specified when the user originally sends tx and keep out of this level?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, so the big difference is that the bid's Id (hash) has to take into account the allowed stores -- unlike execution node address, more like the allowed peekers.
I think that having in mind the direction in which we move, this will supersede execution node address, and I think it should be at the contract level

}

type ConfidentialEthBackend interface {
BuildEthBlock(ctx context.Context, args *BuildBlockArgs, txs types.Transactions) (*engine.ExecutionPayloadEnvelope, error)
BuildEthBlockFromBundles(ctx context.Context, args *BuildBlockArgs, bundles []types.SBundle) (*engine.ExecutionPayloadEnvelope, error)
}

type BuildBlockArgs = types.BuildBlockArgs
type PubSub interface {
Copy link
Member

Choose a reason for hiding this comment

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

I think Topic is a better name

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

@Ruteri Ruteri Sep 18, 2023

Choose a reason for hiding this comment

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

Renamed to StoreTransportTopic

core/vm/suave.go Show resolved Hide resolved
}
)

type RedisPubSub struct {
Copy link
Member

Choose a reason for hiding this comment

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

I think you also need to include a UUID from the node. Last time I worked with redis pubsub, it would pub it's own message back to itself and you are meant to filter these out with the UUID.

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 was thinking to maybe set it to the execution node address, but that would mean nodes can't share it. Uuid is easily spoofed, but we could use both as a way to cut out the messages we send

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Check if you like it better


func (r *RedisStoreBackend) Store(bid suave.Bid, caller common.Address, key string, value []byte) (suave.Bid, error) {
storeKey := formatRedisBidValueKey(bid.Id, key)
err := r.client.Set(r.ctx, storeKey, string(value), time.Second).Err()
Copy link
Member

Choose a reason for hiding this comment

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

We're not putting a TTL/expiry on this. I think that might be okay for now, I would rather worry about Redis getting too large than it expiring out from under me while testing. But we do need to come to consensus on some TTL before testnet and advertise publicly.

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've made it a day for now

@Ruteri Ruteri force-pushed the confidential-store-engine branch from fd926a8 to c054246 Compare September 18, 2023 16:52
return fmt.Sprintf("bid-%x", bidId)
}

formatMiniredisBidValueKey = func(bid suave.Bid, key string) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

reuse across backends?

Copy link
Collaborator Author

@Ruteri Ruteri Sep 19, 2023

Choose a reason for hiding this comment

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

Yeah, I don't think there's any benefit to making miniredis different. Unified

"github.com/go-redis/redis/v8"
)

var ffStoreTTL = 24 * time.Hour
Copy link
Contributor

Choose a reason for hiding this comment

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

24 could be an env var 🤔

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'd avoid giving a false sense of stability by making this an env var - this is subject to change soon


func (r *RedisStoreBackend) Stop() error {
if r.cancel == nil || r.client == nil {
panic("Stop() called before Start()")
Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm perhaps better return an error instead of panic? i think panic shouldn't be used in live code if at all possible (and if used the function should be prefixed with Must)

Copy link
Contributor

Choose a reason for hiding this comment

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

in this case, you could just silently return too

return nil
}

func (r *RedisStoreBackend) Stop() error {
Copy link
Contributor

Choose a reason for hiding this comment

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

if error result is unused, remove

Copy link
Collaborator Author

@Ruteri Ruteri Sep 19, 2023

Choose a reason for hiding this comment

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

It's implementing the node.Lifecycle interface
I'll maybe log the error if any appear?

log.Trace("Rebids pubsub: publishing", "message", message)
data, err := json.Marshal(message)
if err != nil {
panic(fmt.Errorf("could not marshal message: %w", err))
Copy link
Contributor

Choose a reason for hiding this comment

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

no panic please

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

On the other hand, under what circumstances would this be the case?
Added a log + early return


func (r *RedisPubSubTransport) Stop() error {
if r.cancel == nil || r.client == nil {
panic("Stop() called before Start()")
Copy link
Contributor

Choose a reason for hiding this comment

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

no panic please

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For this one I'm kind of on the fence. This should never be the case, and would only appear if someone introduced a bug - in which case a panic is the quickest way to spot the error.
On the other hand it's not critical so it'd be better to not terminate.
I'll return errors

func calculateBidId(bid types.Bid) (types.BidId, error) {
copy(bid.Id[:], emptyId[:])

body, err := json.Marshal(bid)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a blocker but I would consider doing keccak(rlp(bid)) instead to have the same standard as the other hashed objects in the 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.

I'd defer to a later point, probably once we'll consider having another implementation - right now the id is localized to this function, and I'm not sure if bid will rlp-encode straight away

transportTopic StoreTransportTopic
mempool MempoolBackend

daSigner DASigner
Copy link
Member

Choose a reason for hiding this comment

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

for clarity can we add a comment that "DA = data availability"

Copy link
Member

@dmarzzz dmarzzz left a comment

Choose a reason for hiding this comment

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

LSTM

@@ -298,26 +303,15 @@ type ConfidentialEthBackend interface {

The Confidential Store is an integral part of the SUAVE chain, designed to facilitate secure and privacy-preserving transactions and smart contract interactions. It functions as a key-value store where users can safely store and retrieve confidential data related to their bids. The Confidential Store restricts access (both reading and writing) only to the allowed peekers of each bid, allowing developers to define the entire data model of their application!

The current, and certainly not final, implementation of the Confidential Store is managed by the `LocalConfidentialStore` struct. It provides thread-safe access to the bids' confidential data. The `LocalConfidentialStore` struct is composed of a mutex lock and a map of bid data, `ACData`, indexed by a `BidId`.
The current, and certainly not final, implementation of the Confidential Store is managed by the `ConfidentialStoreEngine`. The engine consists of a storage backend, which holds the raw data, and a transport topic, which relays synchronization messages between nodes.
We provide two storage backends to the confidential store engine: the `LocalConfidentialStore`, storing data in memory in a simple dictionary, and `RedisStoreBackend`, storing data in redis. To enable redis as the storage backed, pass redis endpoint via `--suave.confidential.redis-store-endpoint`.
Copy link
Member

Choose a reason for hiding this comment

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

perhaps just add a note that Redis is only intended for first testnet and as an upgrade path for a future p2p based protocol

@ferranbt ferranbt self-requested a review September 28, 2023 12:10
@Ruteri Ruteri merged commit 7568b20 into main Sep 28, 2023
@Ruteri Ruteri deleted the confidential-store-engine branch September 28, 2023 12:31
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