o1-labs / o1js

TypeScript framework for zk-SNARKs and zkApps
https://docs.minaprotocol.com/en/zkapps/how-to-write-a-zkapp
Apache License 2.0
491 stars 107 forks source link

decouple archive-related functionality / pluggable persistence strategies #1589

Open harrysolovay opened 4 months ago

harrysolovay commented 4 months ago

A critical selling point of Mina is that it is the proof layer. Not the storage or compute layer. It is not a blockspace platform. This distinction seemingly promotes a future in which developers can use their application platforms of choice, and leverage Mina solely for modeling computation such that properties of its execution are provable. I imagine an ecosystem of adapters, which enable users of o1js (and therefore Mina) to seamlessly persist their state to a wide range of services. To get to this future, I believe we need to rethink some of the current baked-in opinions regarding "archive" services and related APIs (such as that of event emission and retrieval).

I believe we'll spare developers significant confusion by removing the existing event API and instead focusing our efforts on enabling developers to hook into the contract execution with their own off-chain functionality. Consider the following example.

import { method, SmartContract } from "o1js"

export class MyContract extends SmartContract {
  events = {
    Hello: Field,
  }

  @method
  async hello() {
    this.emitEvent("Hello", Field.empty())
  }
}

What is the this.emitEvent call really doing? Currently, it appears to call into an archive service, whose endpoint is specified in the Network factory. I believe a less opinionated approach would be preferable.

import { method, SmartContract, dep } from "o1js"
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"

export class MyContract extends SmartContract {
  @dep("someKey")
  emitEvent = dep<EmitEvent>()

  @method
  async hello() {
    await this.emitEvent("First", Field.empty())
  }
}

type EmitEvent = (event: {
  name: string;
  value: Field
}) => Promise<void>

When initializing the contract, one would then specify the EmitEvent dependency, which––in this case––calls into AWS SQS.

import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"

const client = new SQSClient({
  region: "us-east-1",
})

const contract = new MyContract("...", {
  emitEvent(event) {
    return client.send(new SendMessageCommand({
      QueueUrl: process.env.QUEUE_URL,
      MessageBody: JSON.stringify(event),
    }))
  }
})

This has the added benefit of enabling developers to specify services on a per-contract level. I'd imagine this will frequently touch on local storage, especially as local-first and peer-to-peer tooling evolves (shoutout @schickling + see https://twitter.com/localfirstfm).

const contract = new MyContract("...", {
  emitEvent(event) {
    localStorage.setItem(
      MY_EVENTS_KEY,
      JSON.stringify([
        ...JSON.parse(localStorage.getItem(MY_EVENTS_KEY))
        event,
      ])
    )
  }
})

Note: there are tradeoffs. For instance, the handling of failed transactions (the failure of which occurs without the persistence adapter's awareness). However, this is currently an issue anyways (albeit less of an issue), as an archive service or Mina node proxy could both go down.

yunus433 commented 4 months ago

It is definitely a great point that archive nodes are not an ideal solution for event emitting, like @harrysolovay mentioned. Any off chain service (either decentralized or centralized) can be used to publish a public event. Nevertheless, maybe a more interesting question is that do we even need events in Mina smart contracts? Mina is ideally designed as a verification layer: It does not perform on chain computation and the storage is very limited (that I believe should only be used for storing data related to proof verification). In an application’s architecture, both centralized or decentralized, do we need a verification layer to emit events to communicate with other layers? The verification module is always used by a prover party, and it is a reasonable assumption to make that this prover can emit its events to the verifier module to notify that the verification layer is now updated to perform any verification related to its action. I think o1js developers should be aware of this architectural difference and model their zkApp to only be used as a verification module. We do not need to put any further responsibilities on top of a verification module, like emitting events.

Please note that this is only a question, and I am not sure if this is the case for all ZK applications in the world. Still, I think this stays valid for most of the use cases.

mitschabaude commented 4 months ago

We do not need to put any further responsibilities on top of a verification module, like emitting events.

I think of events as a custom public input of your zk proof.

The public input in a proof is like the message you sign in a signature: It's some data that the proof is tied to.

Zkapp proofs have an account update as their public input. So the proof says "i'm valid for this account update, and no other".

Most of the account update consists of Mina-specific data, like your balance change. ("I approve this balance reduction on my account"). However, what if you want your proof to make a statement about something completely different, that's not part of the protocol?

Well, events are a little slot in the account update where you can put anything you want. The protocol will simply ignore it. So they are exactly fit for this use case: you want to tie your proof to a specific statement that is not one of the pre-defined statements inherent to the zkapp protocol.

harrysolovay commented 4 months ago

maybe a more interesting question is that do we even need events in Mina smart contracts?

@yunus433 great question. @Trivo25 and I have discussed this a bit in #1569. Short of the possibility of ~topics (as they apply to Mina), I'm uncertain where events add value.

Well, events are a little slot in the account update where you can put anything you want. The protocol will simply ignore it. So they are exactly fit for this use case: you want to tie your proof to a specific statement that is not one of the pre-defined statements inherent to the zkapp protocol.

@mitschabaude I'm not sure I follow. Are "custom public input[s] of your zk proof[s]" not possible through other explicit means? Can you please provide some examples of where this would be useful to app developers?

yunus433 commented 4 months ago

Hey @mitschabaude, this is a great point that I definitely agree: being able to prove any associated public data to your proof is definitely fundamental. Nevertheless, I think the first point I made is still valid. I can use a merkle tree or hash list etc. that is stored in the smart contract to associate any public data to my proof. I can again safely assume that the prover party will be providing the data mapping to this hash in some other way to the verifier, as this is the whole reason why s/he generated proof at the first place.

I know this may sound like an unnecessary use of the on chain data, but I think this approach is safer than using events, as the liveness guarantees can be changed according to the needs of the prover. As far as I know events are using archive nodes, and I think archive nodes' cannot be considered fully live.

mitschabaude commented 4 months ago

sorry @yunus433 but that doesn't make sense to me

this approach is safer than using events, as the liveness guarantees can be changed according to the needs of the prover

by putting data into an event you don't magically lose the ability to also provide that data through other means than an archive node. the liveness guarantees of anything can be changed according to your needs