New Ethereum talks, every Monday. The week's conference uploads by event, in your inbox.

zkProving the history of Ethereum in real time.

Devcon 7 SEAWed, Nov 13, 2024, 06:03 AM · 26:44

I'll explain the current work that we are doing in the Polygon zk teams to improve the performance of the provers and the quality of the tooling. I'll will explain how we can parallelise the generation of the proof and how we can integrate with different hardware and software so that it should allow to build a zk proof of a block in real time. I'll explain also how this proofs can be recursively linked to build a zkProof that can proof the whole Ethereum history from the genesis.

Transcript

Hello everybody. We need fast proving. At the beginning was we need proof, we need to generate proofs. At some point we need to generate cheap proof. This was the beginning of the ZKVM.

But what we are seeing now is that no proofs are, I would say, cheap enough, especially if you are doing a roll-up and you have transactions. But we need to prove them fast. Why we need fast proofs? I would say for many reasons. The reasons, I mean, in Polygon, for example, we need it very much for the Act Layer.

When you want to transfer funds from one chain to another chain, two things need to happen. First, one chain needs to commit to one state so that the other chain can use the funds of the first chain. And the other thing is that the second chain needs to be sure that this state is correct. So it has two options. The first thing is that the second chain just follows that chain and then it's warranties that this state is correct.

Or the other option is that the chain A provides zero knowledge proof that his state is correct. In the first case, well, it works well maybe if you have two networks, but if you have hundreds or thousands of networks that are appearing or disappearing every time, it does not scale well. I mean, you cannot ask all the validators of all the chains to run a validator of all the other chains that are in the aggregation space. So the way to scale that is by having a zero-knowledge proof that warranties that the state is valid. But then this means that the user, when he's doing some interchain communication, in the user interaction, meaning the user in the UX, you will need to wait for this time.

So we need to generate these proofs as fast as you want. Other contexts, I mean, yesterday in the presentation of the VIN chain, was a ZK chain, so that means that in this context you need a lot of places. Like clients needs to validate, so needs to prove that the full execution chain, it's another case that's important. So in many cases, this chain, so generating fast proof is important. So how fast can we go?

And I want to present here, so an internal project that we have been working for the last five, six months inside Ethereum. We call it internally Zisk. And it's very much about that. It's how much we can push the proving system. Zisk is also based, it's a system that can prove, I mean, you can build a programming Rust.

It's a RISC-V compiler. It's RISC-V 64-bits. It's a little bit different from other projects. But the full idea of this project is how fast can we move. How fast can we generate this proof?

These actually are these three different, I would say three different pieces or three different projects itself. One is the prover itself. Prover is a generic prover. So the idea is that you can build your own prover that can prove anything. We say it's kind of hardware layer.

We have the PIL-2. PIL-2 is based on PIL-2 is based on the PIL-1 language that we use in the ZKVM and allows you to do an arithmetization in their own way. We extended a lot of the we extended a lot the PIL-2 language the PIL-2 language making them more easy for auditing, deserthimization, and mainly including one thing that's called VATCOPS. That mainly means that instead of having monolithic proof, we can divide the proof in sub-proofs. This is very important because then we can parallelize very well.

So we have a proof. We divide the work in different sub-proofs, and then we can parallelize very well. So we have a proof, we divide the work in different sub-proofs, and then we aggregate them together. This is the main idea of how we can accelerate the things. So the prover, I mean, it's a generic prover.

So how it would work. This is the process that if you want to build any proof, it can be from a Fibonacci series, if it's kind of a hello world, until a VM project that's in there, or maybe some, whatever you want to build. Mainly, you need to build two things. So, we need to build that embeddization. That embeddization is in this, so we just write the pill.

So, it's that embeddization itself. You need to write what we call the Winnes computation. This is at the end is filling, it's a program that's actually filling the trace of this. And that's very much, and with that you already have a prover. So with this, well, you compile the pill, okay?

So you get a kind of a pill out format. You get the rust. You compile a library. I mean, you get a dynamic library. From the pill, you get some helpers so that allows you to build this Windows computation easily.

And then here, we just run a normal setup. Here, the setup is the configuration of the prover itself, some Starix configuration. That's just a configuration file. Here, we generate a normal approving key and a verification key. and then we have the normal thing.

We have a prover, for the prover has an interface that you get an input, and this prover generates the proof and the public outputs, and you can take the proof and the public outputs and verify in the verify. You see that's got it here, okay. So this would be the schema that would work. The thing is that this prover is generic. I mean, you don't need to recompile the prover.

It's just a program that's running there. But it can be a program. It can be a service. It can be, I mean, this program can have, like, different versions. I mean, it can be in GPU.

I mean, it doesn't change. It's generic for any... The idea is that it's generic for any circuit. This program is open source. And everything is designed to minimize the latency.

We will see later the different things related to that. So, and then, let me just go back. So, this would be the prover, okay? But on top of the prover, we have a specific circuit. So, it's just one.

So, we are using, it's like a specific circuit using PIL-2-1. That's implemented in PIL-2-1. We can use the prover. That's actually this disease is this let's say risk 5 emulator kind of actually is not I Would not call it an emulator is a ZK processor The thing the only thing is that there is a direct conversion between RISC-V or even WASM or other or LLVM to this ZK processor. It's a processor that has only about 50 polynomials so it's very very lightweight processor and really fast and really fast processor.

Okay so and then it works very much like any other VM in there. So, you can build a program. So, you build the circuit in a normal program. Of course, you can test your circuit. At the end, it's a deterministic program.

So, you compile the circuit, you get a program, and then you have an input and an output. It's a deterministic program that generates a circuit. Once you have that, that's the only thing you need. You can take this program, you can generate, you can compile using Rust and some setup, and then you get a kind of a ROM, or if you want an ELF kind of a program itself. And then you can use the prover, I mean the prover that we saw before.

Of course here the SISC, the private key, the verification key is already done because this is a specific application, a specific circuit for the prover, but the prover is exactly the same. And then, of course, you have an input, a proof, an output, and a verifier. The only thing is that the input includes the private input and the ROM, so you can prove any program that you want. And of course, in the verifier, you can use the ROM hash, so you can use the verifier itself. So let's go deep in how the architecture, I mean, how this circuit works.

Well, as I told you before, this circuit is based on VATCOPS. Actually, there are many sub-proofs. This sub-proof, there are different kinds and this works very much like a normal processor. We have like a main processor with a program with a ROM and then we have a RAM, we have arithmetic operations, we have a lot of co-processors that can be connected here. So, each one of these is one sum proof.

And actually, you can have, for example, arithmetic. If you have a lot of arithmetic operations, you can have a lot of subcircuits and all them add them to the bus, and they connect them together. This bus, I mean, it's not an electronic bus, but it's something, I would say, similar. I mean, it's more an algebraic bus, but this we are using here lockups, and here you can have different sub-proofs you can have the main processor, you can have the arithmetic processor, each one is like sending things to the bus, and the main processor says, okay, I'm assuming that this arithmetic operation is correct and then there is the arithmetic coprocessor that's saying, I'm guaranteeing that this arithmetic operation with these values is correct. So the full system is correct.

So one is adding to the bus, the other is subtracting to the bus, and at the end, the bus means zero if everything is warranted. And the cool thing is that, I mean, in this bus, I can add arithmetic operations, I can add memory instructions, I can add memory instructions, I can add whatever you want. We have a kind of identifier, but with the same polynomial, we are just adding and subtracting different things altogether. You see the arithmetization. I'm not going to go in detail on the arithmetization, but I give you the big ideas.

I mean, the processor is not based in registers. Registers in ZK are relatively expensive, mainly because in one instruction, you are using in general one or two registers. So that means that there is, like, if you have 32 registers, you may have, like, 30 columns that you are not even using in that line. So doing registers in ZK is quite suboptimal. So the main idea here is, OK, we have like, so in each instruction, what we do is we go directly to memory.

We are doing to access to memory. We are reading to things, and we are storing back to the memory. And this is the main idea. So we have only one register. Actually, we have a couple of registers, a program counter, a kind of a frame pointer or stack pointer in there.

We have this main instruction that can be any instruction there. So we have a way. This is a way to load the first register, load second registers. I mean, you can load from memory or you can load maybe from an immediate value or other things. Maybe you can take just the preview C and things like that.

Okay? We store the memory. In general, we store to a memory. We can also, for example, store to a program counter, so we can do a dynamic jump in there. Okay?

And we have the operations. The operations at the end is just throwing to the boss the operation that you want to build. So the main machine is quite simple in there. And then we have some conditional jumps. I mean, there's all this logic there.

So at the end, we are just... So the operations is A plus B equals C, maybe with a carry or with some flag where you can do a jump to one line of code or a different line of code. So, this is a little bit the deepness, the big idea of the FISC itself. Okay? And, as I told you, this goes through this bus.

Okay? As I told you, this goes through this bus. And one of the advantages, and I want to see here, I won't give you details about why this architecture is very optimal. For example, when you have a multiplication, so this processor is sending that, and the arithmetic is also somehow proving that specific operation in there. So you can have this arithmetic operation.

But one thing, one optimization that we can do is, OK, for example, there are a lot of operations that are usual. When I'm doing, for example, 1 plus 1 or 3 plus 1, I mean, I'm doing small numbers. I'm going to do a lot of them in the processor. So instead of proving all of them in the processor, I can have another machine that's kind of a cache where I have pre-computed operations that are used quite often and then so the processor is always sending the same thing but then these basic operations can be sent, can be proved back to the bus. So that means that for these small operations or for these usual operations, the cost, I mean the number of cells that we are doing are very low.

We can extend this idea. So we can have this processor. Imagine that I have operations. I can have a usual operations table. I can have operations that are doing, so a specific state machine that is doing 32-bit operations and another state machine that are doing 64 state machine operations, just to save here, just to save space.

This idea of work works, for example, for memory, for the memory alignment. I mean, what I'm doing in general, most of the are doing reads and writes that are aligned. Well, no problem. The processor is doing some memory operation. And the memory that just works in 64-bit words, then you're doing a read, a line read, you get it from the memory, a line read, you get it from the memory.

But what happens when you are reading an align net? Well, we can have a special machine that's solving these unaligning things, and actually it's, okay, so you want a non-aligned operation. This solves. So the arrow to the bus means adding to the bus. The other arrow that goes in the other way is subtracting to the bus.

So, okay, I'm proving this, but in order to prove this, I need to do two reads operations. And, of course, some logic for putting these two reads in the same way. And then, of course, the same memory machine is doing these two reads in there. This allows us to do these kind of techniques. It allows us to do, for example, continuations in a very easy way.

So if I have a processor, I can divide the execution in different slots, in different phases. So this is the first 100 clocks, second 100 clocks, and so on. So I can divide them. But at the end of the processor, so I need to connect. I mean, the next slot needs to have the same state that when I finish.

Well, what I can do is just send this state to the bus and recover this state from the bus and then connecting all them together. The final one, the first one, this is because it's cyclic. I mean, it's decay in general, but it's cyclic, and that disconnects. The same with continuations in the RAM. Continuations in the RAM, of course, is not time frame.

In the RAM, you want more address phase. So, I can divide the proof of the RAM that can be big in smaller pieces. But the idea is the same. I have a state transition function that goes line by line. But from the one chunk to the next chunk, I can send the state of the RAM to the bus, recover the state of the RAM to the bus.

recover the state of the run to the bus, the last address that you are reading or writing, and then you can continue. So we can split also, for example, the run in there. So this allows us to divide all the work. I mean, there is no state machine that you need the big thing, so you can divide always the work in everything. Here, as I mentioned, there is a direct conversion.

This is important. Direct conversion, that means that you can use Rust or any other compiling language. It's 64 bits. You can use Golang, or you can use C++, or you can use even Java. I mean, there is 564 so RISC-564 bits is quite standard in there.

And as I told you, there is one direct conversion, one-to-one, from RISC to CISC. We also have a kind of a side project that we are evaluating, converting from WASM to CISC, and even directly from LLVM to SISC, this is perfectly possible, and it can be quite optimal, because when you go to intermediate code, you don't need these registers, and then you can have better code in the processor. But in general, the cost of the proof, of course, the processor is important, but I would say that the operations that are doing the processor are even more important in general. So also the, I mean, well, it's not that you are optimizing the processor and then you get a better proof. You need, it's the whole thing that you need to optimize.

Okay? Well, fiscal advantage, we already talked most of them. Okay? It's low latency, low latency, low latency. And yeah, we integrate with the recurser.

We didn't talk about the recurser, but the recurser is the idea is that you can take any proof and then you can build kind of automatically a proof that aggregates both proof. For example, if you are proving two blocks of Ethereum, you can get a proof that actually aggregates this proof. And we have this recursive that's the tooling that allows you to do that automatically on this. So latency, how we push hard on the latency. Actually, there is four pieces.

I have not much time, but I'm going to try to go fast. The first one is the one that you cannot parallelize. It's like, okay, if I want to prove a computation, I need to compute that thing. So I cannot parallelize that computation very much. And also, if we are emulating for example, I'm doing an emulation, so here I can go slow.

But we are trying this to go as fast as we can. Currently, we are at 80 megahertz, so we can run a processor running, let's say, the minimum trace that you need then to generate the Windows computation. We can go, I think that there is margin here for at least one order of magnitude improvement. Then we have the Windows computation. The Windows computation is already done in parallel, so this can be parallel paralyzed very much.

And you can even start building this Windows computation while you are executing that. Also, the Windows computation, the idea is that we are doing that work in the worker. We do not have a central prover. We can do that in the worker. That means that here we limit a lot the bandwidth.

So we don't have bandwidth requirements. It's minimal, the quantity of information that goes between the workers or the coordinator and the workers. Then we have the sub-proof building. Actually, this is the high work. But the cool thing is that because we have control of what's the size of the sub-proof, then it's just a matter of putting more sub-provers.

Of course, if they are cheaper and faster, you will need less sub-provers. If they are slower, you will need more. And it's very much a matter of a cost, but not a matter of a latency, because we can control very much this proving time. And then the other last is, okay, then we need to aggregate. This is probably currently where we have the bottleneck, but the good news is that we are getting much, much, much better.

And this number, 10 seconds, we already get lower to that. Here, as mentioned, Plancky 2 has a very good circuit for there, and optimizing this aggregation circuit is super important. There is a talk, I think in one hour or so, I think it's in this room, that will give more details on this optimization, parallelization, all the proofs that we are running in a supercomputer and doing experiments, and see that all these numbers get very, very good and these scales very well. Okay, future plans, a lot of things to do, a lot of things in the Balalock, a lot of optimizations that we want to implement. And you can try it, it's working.

Right now we have a basic thing, it's working. The last time that I said that in this scenario, somebody created a Tornado cache and was in a nightmare. But just this is work in progress. So I would say at this point it's in the status that it's working. It requires some refactors of some pieces, some cleanups, some documentations and we hope that, let's see if in the upcoming weeks everything looks nicer and we want to build the...

it has no precompiles, I mean these state machines are limited but you can build any program and you can prove any program for these precompiles or for these specialized state machines, we take maybe two, three months still to complete. Okay, and that's very much my presentation. Enjoy it. Thank you very much. Let's go through some questions.

So what issues did you run into using RISC-V64 that RISC-V 32 wouldn't have? I mean, well, we have been checking what's better, what's worse. Here there are pros and cons. I would say that the big pros is that when you are doing, especially when you are running Rust, you are doing a lot of mem copies. I mean, you are moving a lot of memory information.

And when you are using 64 bits, this is much, much, much more optimal. I would say this is one of the things that we saw that there is a lot of copies in moving back and forth in a place. And because also the current systems are 64 bits, so everything is much more standard. For example, if you want to build, I mean, if you want to use Rust, it's 32-bits, it's okay. But if you want to do Go, for example, you cannot do it in 32-bits.

So you can do it in 64. So 64 is also much more standard. But again, the different tests that we did is that we're slightly better doing in 64 bits. So that's why we stick it at 64 bits. And how will this be different than SP1 RISC-0?

Well, we still need to do some benchmarks and compare, but here the thing is that we are designing not for cost. I mean, that we are here, we are going to push as hard as we can in doing the speeds as fast as we can. So it's difficult to compare yet. Also, we have some of these machines, these special state machines that need to be finalized. But, I mean, the numbers that we have seen is that we are much faster, especially in CPU.

We are working very much in the GPU, and the numbers are quite promising. But, again, I don't like to talk yet about the numbers because it probably would not be fair, and things need to be more settled. It's probably on both sides to start getting some real comparisons. SPEAKER 1- From a developer experience point of view, what does the developer need to do to encode the chain rules into Zisked? SPEAKER 2- Just write a rascalt.

Just write a program that do whatever rules that's in there. Just a side note here is take care on the security of that. When you're getting some input and getting some output the input you can still have soundness problems if you are not writing the right way so you need to understand a little bit how the things work even if you are writing a program in Rust because there are some security issues that can happen there but yeah but at the end it's writing a Rust code. And are there any drawbacks that come with CISC? I mentioned these security things, but this is common in any VM.

You still need to understand a little bit what's behind, but it's much more practical and it's much easier to write a Rust code and out it in RAS code than not an assembly written in some specific special processor there. Don't all these improvements by built-specific hardware, are they better than making optimizations on VMs? I think that's what that says. Maybe. If they do, we will implement them.

But they are not hard to build this hardware. I mean, this is not like real hardware that you need to build like machinery. This is just a program. And it's electronics. You need to have them the right way.

And once you have it, they're already there. I don't know what these mean. So maybe you could read them out if they make any sense to you. Yeah, this is probably the context. But this is, at the end, is the position of the registers because we are emulating registers in RISC-V.

So we are setting specific positions in the memory that actually are the registers. And this is just defining the constants that defines the registers. There's a question in there. Yeah, that's very much the thing. I mean, we can give you the detail.

I mean, there is on that, but that's it. Does the developer need to write PIL-2? Not at all. Not at all. Not at all.

Unless you want to build maybe some specific state, some specific state machine, I mean, some coprocessors or things like that, that then you can extend that way. But the idea is that the final person that's writing in Rust is writing Rust, and that's it. Great.

Automatic transcript — names and jargon may be misspelled.