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
477 stars 107 forks source link

automatically initialize reducer #1569

Open harrysolovay opened 2 months ago

harrysolovay commented 2 months ago

Multiple reducers are seemingly not supported. Yet, the way in which one initializes their reducer suggests multiple reducers are supported. Ie.

export class MyContract extends SmartContract {
  reducerA = Reducer({ actionType: ActionA })
  reducerB = Reducer({ actionType: ActionB })
}

Thoughts on automatically initializing a reducer based on the presence of an action field?

export class MyContract extends SmartContract<Action> {
  action = Action
}

Note: the type param on SmartContract would be necessary to get action-specific typing.

Then, devs could utilize the reducer as they currently do (albeit potentially from a different name "reducer").

@method
rollup() {
  const state = this.reducer.getActions({ fromActionState })
  //            ~~~~~~~~~~~~
  //            ^
  //            Would be consistent across all contracts (less cognitive load)
}

On one hand, this means devs can't use the name "reducer" for other members. On the other hand, the name is consistent and therefore easy for developers to recognize.

This API change would also make actions look more similar to events. Their APIs could even converge (perhaps part of the path to polymorphic actions / #1558). Does one even need to differentiate between actions and events? When emitting, the developer could specify whether they want it to be an action / whether that state should be accessible within the proof context. But I digress.

Trivo25 commented 2 months ago

It is quite confusing indeed! Personally, I would like to keep the idea of multiple reducers within one contract open, especially since we are considering adding topics to reducers in the near future - this could include supporting not only multiple topics, but also multiple reducers.

This API change would also make actions look more similar to events.

I would like to discuss this further! Fundamentally, actions and events are two different things - one is being committed to on-chain and the other one isn't. Arguably it leaves room for confusion for new developers. However, I like the idea of an explicit distinction between the two, but I am also interested in how we can leverage events to serve as a more functional and practical feature.

harrysolovay commented 2 months ago

I would like to keep the idea of multiple reducers within one contract open, especially since we are considering adding topics to reducers in the near future

Fascinating! Can you please point me to any discussion relating to topics / share your thoughts? I'm also wondering how #1558 would impact this design? It seems like we want to distinguish between these emitted events/actions/topics... however, I don't think the Reducer-initialized container should be the means of distinguishing. Moreover, what differentiates an action from an event (other than its availability in the proof context)?

class MyAction extends Struct({
  action: Field
}) {}

class MyEvent extends Struct({
  event: Field
}) {}

export class MyContract extends SmartContract<MyAction | MyEvent> {
  @method
  async sayHi() {
    // to enable processing within contracts:
    this.commit(
      new MyAction({ action: Field.empty() })
    )
    // to associate with a given topic:
    this.commit(
      new MyAction({ action: Field.empty() }),
      "topic-name"
    )
    // to emit as an event
    this.event(new MyEvent({ event: Field.empty }))
  }
}
harrysolovay commented 2 months ago

It strikes me that events are reliant on archive nodes. Instead of a mechanism for emitting events, what we really need is a way to inject your own event emission logic (or other arbitrary logic). This way, you can plug into your own db/notification system/misc. services.

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

export class MyContract extends SmartContract {
  emitEvent = Fn<MyEvent>()

  @method
  async sayHi() {
    await this.emitEvent(new MyEvent({ event: Field.empty }))
  }
}

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),
    }))
  }
})

Dependency injection strikes again.

Trivo25 commented 2 months ago

Fascinating! Can you please point me to any discussion relating to topics / share your thoughts? I'm also wondering how https://github.com/o1-labs/o1js/issues/1558 would impact this design? It seems like we want to distinguish between these emitted events/actions/topics... however, I don't think the Reducer-initialized container should be the means of distinguishing.

While there's no spec or RFC in place yet, I'm happy to discuss the core concept. Currently, dealing with diverse logic for various topics can be constraint intensive, as all operations must be predefined within a static computation. This results in having to manage separate implementations simultaneously within the same circuit for each topic, leading to high constraint and proving costs, overall limiting the user in what they can do. The fundamental approach of supporting different topics comes very close to your Enums idea, but with a small twist: rather than embedding the "Enum switch logic" within the circuit, we delegate it to a recursive proof. This allows us to dynamically determine which topic to execute and prove, effectively ignoring circuit overhead that you would normally get. In other words, while adding topics currently increases constraints linearly with their number, using recursion ensures these constraints remain constant - no matter how many different topics, implementations or Enums! There's much more planned that will make this easier to use. Right now it's a raw "message queue", in the future this will be hidden behind all sorts of powerful APIs.

Moreover, what differentiates an action from an event (other than its availability in the proof context)?

This is pretty much the core idea. Actions are committed to on-chain, whereas events aren't. This means you cannot refer to previously emitted events within a smart contract, since there is no on-chain commit - hence you cannot prove events were previously emitted by your smart contract.
Disclaimer: I think there's potential in levering Events more, we have talked about this internally and also tease it on the docs afaik. We just haven't gotten around to actually utilizing this primitive yet.

It strikes me that events are reliant on archive nodes. Instead of a mechanism for emitting events, what we really need is a way to inject your own event emission logic (or other arbitrary logic). This way, you can plug into your own db/notification system/misc. services.

I don't think there is a mutual exclusion here! Every zkApp developer (and user) can run their own archive node to observe the blockchain. You can, however, use the archive as a proxy for your own database solution. It's important to keep in mind that you cannot just store any arbitrary events into your database, as transaction might be rejected or a fork might happen!

Thank you so much, Harry. Let's keep this and other conversations going.

harrysolovay commented 2 months ago

Thank you so much, Harry. Let's keep this and other conversations going.

The pleasure is all mine. & let's!

The fundamental approach of supporting different topics comes very close to your Enums idea

In that case, I'm not so sure if the term "topic" is fitting. In my experience, "topic" implies a means of grouping / listening for specific messages based on the values contained in those messages (not the types of the messages). For instance, I might create a topic to represent actions committed by a specific user. These actions could be different... but they all pertain to that user. This would allow Mina-interfacing programs to subscribe to that user's actions (or in this case, prove something about that user's actions). However, I don't think "topics" should be a means of declaring different action types.

using recursion ensures these constraints remain constant - no matter how many different topics, implementations or Enums!

AWESOME – I'm very excited about this!

Right now it's a raw "message queue", in the future this will be hidden behind all sorts of powerful APIs

AND THIS!

I don't think there is a mutual exclusion here! Every zkApp developer (and user) can run their own archive node to observe the blockchain. You can, however, use the archive as a proxy for your own database solution.

Sounds like an opinion that should not be baked into o1js. I'd much prefer we get rid of any archive-node-related functionality, and instead provide devs a means of specifying adapters for hooking into their own services / means of persistence. We could prepackage an adapter that would utilize archive node services, but I believe this should be explicit. Following this train of though, let's re-raise the question of "why the events API?" How does this help developers who want to bring their own means of persistence (which I believe––in the long term––will be most developers)? Might we want to reverse course on this set of opinions?

Trivo25 commented 2 months ago

In that case, I'm not so sure if the term "topic" is fitting. In my experience, "topic" implies a means of grouping / listening for specific messages based on the values contained in those messages (not the types of the messages). For instance, I might create a topic to represent actions committed by a specific user. These actions could be different... but they all pertain to that user. This would allow Mina-interfacing programs to subscribe to that user's actions (or in this case, prove something about that user's actions). However, I don't think "topics" should be a means of declaring different action types.

The ideas we have should enable both, it's still in its infancy!

Sounds like an opinion that should not be baked into o1js. I'd much prefer we get rid of any archive-node-related functionality, and instead provide devs a means of specifying adapters for hooking into their own services / means of persistence. We could prepackage an adapter that would utilize archive node services, but I believe this should be explicit. Following this train of though, let's re-raise the question of "why the events API?" How does this help developers who want to bring their own means of persistence (which I believe––in the long term––will be most developers)? Might we want to reverse course on this set of opinions?

I definitely don't think we should force developers to use archives, so I agree with you. The point I was trying to make is that events aren't just pushed into a database or archive node - they go through the network and consensus (where they are hashed as well). The archive just makes it easy to observe this data, because it listens to the chain and follows consensus rules (forks, etc). Pushing directly to a database locally would just collect all events, also those that would have never made it to the network! Also, archives are accessible to everyone (both users and developers), whereas databases are controlled by the developers or the host, which makes it easy to censor access. The archive node was just the path of least resistance, for now. Hope that makes sense!

harrysolovay commented 2 months ago

Pushing directly to a database locally would just collect all events, also those that would have never made it to the network!

Very good point, but I believe there are other ways to ensure the source of truth never forks.

archives are accessible to everyone (both users and developers), whereas databases are controlled by the developers or the host, which makes it easy to censor access

Seems strange / misaligned with Mina's core architecture. I'm not saying we shouldn't devise a solution to this... I just don't think the solution should be baked into the o1js framework.


I opened #1589 for us. That way this issue stays about the reducer/action DX + topics.