[Account | User]-centric Private Blockspace

This post outlines a model that addresses valid critiques around Private Blockspace’s present model that has Protocol Operators (POs) in control of encryption key management.
Ultimately, the encryption keys have a DA problem: you must have robust solutions to prevent POs from withholding the data by withholding the keys.

Why Private Blockspace?

The web3 industry is moving away from it’s original form where all state is public. We now are emerging into a world where state is mostly offchain and thus putting the responsibility of maintaining it on individuals a (exclusive) subsets of users, not the entire network.

If user state is never shared, or latter lost by all parties, it is likely that their account is “frozen” and irrecoverable, perhaps even as much as the protocol itself being unable to progress globally.
A good solution to this is to publicly ensure data availability (DA) over that data.

I need DA, but what if we don’t want data to be publicly readable?
We need a way to obtain DA properties on private data.

Goal

Enable user-specific encryption of their state, stored publicly, and always accessible to users and POs even under hostile conditions using Celestia for DA.

  • Users must be able to determine if offchain global state for a protocol includes their account state.
  • Users must be able to selectively disclose and make proofs about their latest state (and may be able to do so for historical state as well).
  • Protocols should ensure user encrypted state is posted on Celestia before progressing global state.
  • Users should be able to progress the protocol by proving a correct STF on their state onchain without any POs involvement - enabling “forced” state updates via proofs on their state (likely to “exit” the protocol, like taking tokens out, etc.).

Desired properties of encryption:

  • Users should have the ultimate control of encryption keys used, rather than relying on POs to create, distribute, and maintain them.
  • Encryption schemes should provide means to enable forward secrecy and post-compromise security properties on state.
  • Existing Key Management Systems (KMS) should be easy to integrate for POs and users.

Initial use cases

A few related ideas:

Why not <some other storage>?

Where should state be stored? IPFS? Matrix? Centralized cloud? All of them?

Some example use cases that need DA:

  • Enforce that a protocol cannot progress private user state data is not made available. Thus you ensure users can always exit, possibly further able to force include any transaction type into the protocol.
    They are guaranteed DA for a 30 day window [1] to be able to get the data needed to exit if the DEX is not cooperating and/or down.
  • Users can make proofs about their private state, know to be publicly accessible where you would be able to build in protocol mechanisms to monitor for such a censorship resistant message on Celestia.
    Example: “I hold X tokens on this private DEX. Force exit these tokens from the protocol.”

Why not just authenticated encryption (AEAD) ?

While you can prove that some public data was embedded into the encryption operation, you cannot constrain the plaintext at all.
You must fully trust the publisher that some associated data like the hash of the plaintext didn’t lie in creating the ciphertext.
It’s quite possible to commit a “hash” of something into the AEAD object, but that hash is not required to be related to the plaintext in any way.

Verifiable Encryption (VE) enforces that some ciphertext contains plaintext with specific properties (like the plaintext hash), and constrains the it to be constructed correctly (using a specific key, nonce, algorithm used, etc.)

Assumptions

Protocol Outline (ZK Enforced)

flowchart TD
    subgraph MerkleHashes["Tree $$\ (T)$$"]
        Root["Root $$\ (R)$$"]
        L1["$$H(H_{1},H_{2})$$)"]
        L2["$$H(H_{3},H_{4})$$)"]
        LH1["$$H_{user=1}$$  = Leaf Hash"]
        LH2["$$H_{2}$$"]
        LH3["$$H_{3}$$"]
        LH4["$$H_{4}$$"]

        Root --> L1
        Root --> L2
        L1 --> LH1
        L1 --> LH2
        L2 --> LH3
        L2 --> LH4
    end

    subgraph POonly["PO known states<br>(NOT on Celestia)"]
        D1["$$state_{user=1}$$"]
        D2["$$state_2$$"]
        D3["$$state_3$$"]
        D4["$$state_4$$"]
    end

    LH1 --> D1
    LH2 --> D2
    LH3 --> D3
    LH4 --> D4

    PD1["$$ZKP(encrypted(state_1), H_1)$$"]
    PD2["$$ZKP(encrypted(state_2), H_2)$$"]
    PD3["$$ZKP(encrypted(state_3), H_3)$$"]
    PD4["$$ZKP(encrypted(state_4), H_4)$$"]

    D1 -- "$$VE(state_{user=1},s\_key_{user=1})$$" --> PD1
    D2 -- "$$VE(state_{2},s\_key_{2})$$" --> PD2
    D3 -- "$$VE(state_{3},s\_key_{3})$$" --> PD3
    D4 -- "$$VE(state_{4},s\_key_{4})$$" --> PD4

    subgraph EncryptedStates["VE of states (on Celestia)"]
        PD1
        PD2
        PD3
        PD4
    end

    PD1 .-> LH1

We publicly disclose the Merkle Tree T: All tree node hashes.
We also only encrypt the leaf values (user-specific state data) as we assume there is no need to hide the remaining tree structure as no state information is disclosed in it.[3]

flowchart TD
    User[/"User"\]
    DA[("Celestia Data Availability")]

    subgraph " "
        direction TB
        PO[/"Protocol Operator (PO)"\] -- "$$verifiably\_encrypt(state_{user}, s\_key_{user})$$" --> EncProofPerUser["User encrypted $$\ ZKP(state_{user}, H_{user})\ $$ packets"]
        PO -- "Tree $$\ T\ $$ of user states, leaves omitted" --> ZKP["$$T$$"]
    end

    ZKP --> DA
    EncProofPerUser --> DA

    subgraph " "
        direction TB
        User -- "$$verify(ZKP) \rightarrow H_{user} \ \ \&\& \ \ decrypt(state_{user}, s\_key_{user})$$" --> UserProof["$$(state_{user}, H_{user})$$"]
    end

    EncProofPerUser -- "Receive update <br/> (happy path)" --> User
    DA -- "Receive update <br/> (fallback)" --> User
  1. PO verifiably encrypts user’s state (state_{user}) with user-specific symmetric key (s\_key_{user}), and commits to it’s hash H_{user} that is included in T with root R .
    • Verify ZKP so users know it’s unmodified upon decryption
  2. PO produces a ZKP for each user with:
    1. Input = state_{user} and associated s\_key_{user}
    2. Hash the state to arrive at identical leaf hash (H_{user})
    3. Commit H_{user} in VE as “anchor” to T that contains H_{user}
  3. PO submits to DA:
    • T
    • ZKP of Verifiably Encrypted user states proving T as public “anchor” that all H_{user} reference,
  4. User obtains ZKP of their encrypted state data from PO (happy case) or DA (if P0 is misbehaving)
  5. User verifies ZKP to obtain $(encrypted(state),H\_{user})$
  6. User decrypts to obtain state_{user}
  7. User knows state_{user} is included in T with root R, thus affirming their state is in T that only POs know globally.

With Blobstream, we may enforce DA happens in protocol, ensuring state data was published on DA layer before progressing on any EVM chain.[4]

User-Controlled Key Management Protocol

In the original vision of VE protocol-centric models, the POs would be the the only parties who had sole access of the symmetric keys to encrypt with, and thus undefined methods for key recovery (threshold decryption, MPC, etc.) was required.
In user-centric models, users must know the keys for normal operations, and thus no key recovery mechanism is generally required as all the keys needed are already shared with parties that need them.

We can go further to enforce that encryption keys are generated and held by users in such a fashion that POs cannot progress without using the user’s defined key selection to encrypt with.

Implementation Proposal

With the goal to be easy to integrate, well supported cryptographic standards and protocols should be use.
The Messaging Layer Security (MLS) is a emerging standard that could be a great fit for use here.
It defines how to couple protocols:

  • Key exchange and management
  • Authentication Services
  • Delivery Delivery

MLS is a bit over complex for one-to-one session that will likely be the most common type of communication, but as implementations exist making using MLS relatively easy, and overhead is not imposed on anything we need to run the the zkVM, it’s a solid basis to expand into multi-party selective disclosure solutions latter on.

Below is a basic MLS workflow to establish a three participant group that includes a single user, a protocol operator, and an auditor.
As an example, we use a Directory Service as some account based blockchain, mapping account addresses to their respective full public keys is needed. MLS Directory Service and Group Channel are represented as teh protocol’s settlement layer blockchain and Celestia, respectively. Specifics of what the Directory and Channels are could be adjusted to fit the use cases.

sequenceDiagram
    participant A as User [A]
    participant B as Protocol Operator [B]
    participant C as (Optional) Auditor [C]
    participant Dir as Directory (L1 PubKeys)
    participant DA as Group Channel (Celestia)

    A->>Dir: Upload KeyPackageA
    B->>Dir: Upload KeyPackageB
    C->>Dir: Upload KeyPackageC

Clients A, B, and C publish KeyPackages to the directory

sequenceDiagram
  participant A as User [A]
  participant B as Protocol Operator [B]
  participant C as (Optional) Auditor [C]f
  participant Dir as Directory (L1 PubKeys)
  participant DA as Celestia (Group Channel)

  Dir->>A: download KeyPackageB, KeyPackageC
  A->>DA: Add(A→AB)<br/>Commit(Add)
  A->>B: Welcome(B)
  DA->>A: Add(AB→ABC)<br/>Commit(Add)
  A->>C: Welcome(C)
  DA->>A: Add(AB→ABC)<br/>Commit(Add)
  DA->>B: Add(AB→ABC)<br/>Commit(Add)

Client A creates a group with clients B and C

Next Steps

Gather feedback from the community on these ideas and plan for proof-of-concept implementation.

:pray: Please comment on this post!
We are eager to understand what issues you spot, and even more so what use cases you would want to pursue with it!

Further Reading


  1. window ↩︎

  2. This could maybe be relaxed with more thinking, possibly requires something like FHE/MPC to do :thinking: … Open to ideas! ↩︎

  3. The overhead of encrypting the full Merkle proofs is redundant and costly if there is no problem revealing all but the leaves publicly.
    (If that is an issue, replacing encrypt(\gamma_{user}, s\_key_{user}) with verifiably_encrypt(\gamma_{user}, s\_key_{user})) in full would work too. ↩︎

  4. Important practical note: with Blobstream integration, POs should never be able to leave any user in an irrecoverable state. BUT this may be impractical to block progress on, as encrypted user data packets need to be simultaneously gathered and all verified. Tooling like the equivalency service could be adapted for that purpose: to prove a multi-blob, and even multi-block inclusion of all those packets into one succinct (i.e. groth16) verification on the EVM. ↩︎

8 Likes

Thank you for your proposal! I have the following questions/ suggestions and I would be very happy to hear your thoughts:

  • The PO proves for every user that they encrypted the same state whose hash appears in the Merkle tree. But do they also prove that they used the correct key—specifically, the user’s symmetric key—to perform this encryption? Such a guarantee would be useful, because, otherwise, a malicious PO could encrypt the state under an arbitrary key, preventing the user from decrypting it later and recovering their state. For the PO to prove that the correct keys were used, there would need to be some form of public commitment to those symmetric keys.

  • If a user tries to decrypt their encrypted state and discovers that the PO did not use the correct key, can the user prove this claim without revealing their secret symmetric key? Furthermore, if such a complaint arises—where a user claims that the PO used an incorrect key—can the system enforce accountability to determine whether the PO was actually malicious or whether the user falsely accused the PO?

1 Like

Thanks so much for the interest & great questions!

  1. Indeed you can use Verifiable Encryption as an end user to publish a key with an “anchored” hash of the key itself, encrypted using asymmetric encryption. [1] Thus everyone can know that a specific key was declared by the user, and subsequently the PO can prove they used the same key in encrypting data for that user. A very rough idea on a protocol:
    1. PO generates keypair
    2. PO publishes pub\_key_{protocol} onchain
    3. User generates a symmetric encryption key they want PO to use
    4. User VE’s their chosen encryption key, using a preestablished public key the POs control a private key for UserKeyProof:
      • VE_{asymmetric}(pub\_key_{protocol}, s\_key_{user}) = Proof(hash(s\_key_{user}), encrypted\_to\_pubkey(s_{key}))
    5. User publishes UserKeyProof on settlement chain, verifier checks against pub\_key_{protocol} for correct encryption from user->PO
    6. Onchain STF enforces that VE produced by POs must use whatever each user set onchain [2]

Thus there should be no way that incorrect encryption or using an malformed key is possible, keeping everyone fully constrained to use keys counter-parties declare ahead of time, and can update (ideally) at any time. (That would be the goal at least! We would surely need to refine the draft protocol here)

Please do let me know if you have any feedback on this, and/or any further questions @apstouka!


  1. The caveat here being that VE as it’s presently implemented is symmetric only, so a new asymmetric encryption zkVM program or other solution would be needed targeting end user devices. This may be impractically heavy computation at this time… But that said, the user only needs to do this when they want to update a key, so could be as little as one time (but really should be done semi-regularly) ↩︎

  2. I would think this would be a more natural fit for a contract where settlement is taking place - so user set key hashes can be directly compared to what the PO publishes & rejected if there is a mismatch on attempting an update, but this could also be some aggregate proof offchain, verified onchain too. ↩︎

Thank you very much for your response! Using VE to ensure (i) that the user encrypted correctly the symmetric key and that (ii) PO encrypted the state of every user using the correct symmetric key seems to me a good direction in terms of security. One concern I retain is the efficiency of this approach, since in case (ii) the PO must use VE for every user and for every state update.

1 Like

Good observation! I think for the likely-to-be-infrequent updates from end-users on their keys, the proving time is less important, and we are only encrypting a key. For the bulk user data, I am confident there are ways to drastically reduce the overhead of proving for VE such that this would not be a bottleneck for realistic apps with ~seconds per state update, but in the present implementation using a zkVM to fully encrypt data, it likely is.

Some initial ideas:

  • (minor proof cost reductions, better parallel processing) Avoiding overhead of per-user VE job: VE program modified for most optimal bulk encryption - ideally streaming with proof aggregation as needed. (for very rough benchmarks see the initial testing and close-to-present SP1 program)
  • (large proof cost reduction, increased complexity & perhaps trust) Rather than fully proving all encryption, you can prove a random subset of encryption on a larger batch of data is correct, and sample it. As ChaCha is a stream cypher, we can cheaply encrypt random segments, I would think a carefully crafted Fiat–Shamir heuristic here could assist. Thus
  • (radical new scheme, for “zero” overhead VE when posted to DA) Some preliminary ideas mentioned by Lev Soukhanov {telegram} in the ZK Podcast chat (here) around using a ZODA-like construction for “free” encryption on proof of correct encoding (that is far easier to prove)

    You encrypt RS encoding of your cleartext (padded with some blinding factors), with individual values being selectively decryptable.
    And then you run normal FRI, with 1st round using this ciphertext instead of Merkle tree, and all the rest running normally.
    So zk is from zk of the original construction. And encryption validity is also from original construction because it guarantees you are close to a codeword.
    (selective decryption can be achieved in few ways, simplest one is computing pads that you use to respond to 1st round queries in a small accompanying zk argument)
    So compared to naive version this has almost no overhead. It is almost like zoda’s “accidental computer”, but here we have “accidental encryption” at no overhead.

If there is some real demand for account-centric Private Blockspace, these directions are surely to be explored :grin:

Yes, I agree that efficiency optimisations are more important for the VE used by PO to prove that they used the correct key to encrypt the new state of the user, because this process must occur every time PO updates a user’s state, not only when the user needs to update their symmetric key (e.g., for security reasons), as in the first use case of VE :slightly_smiling_face: .

1 Like