# Optimize zkEVM throughput: Series II | Devcon SEA

- Channel: [Devcon](https://streameth.org/devcon)
- Date: 2025-10-07
- Duration: 1:16:09
- Watch: https://streameth.org/watch/yt-EdUfOvoIhNc
- YouTube: https://www.youtube.com/watch?v=EdUfOvoIhNc

## Description

There are different ways to optimize the zkEVM, the one exposed in this workshop is through optimizing the zkASM (zk assembly) code itself so that it consumes fewer counters for the same execution.
The first 40min of the workshop is a deep explanation of the zkASM language, instructions, operations, counters, build... And the rest of the time we will be live coding and explaining in detail two optimized core functions of the zkEVM so that attendees can appreciate the before and after optimizing

Speaker(s): Ignasi Ramos, Carlos Matallana
Skill level: Expert
Track: Layer 2
Keywords: ZK-EVMs, EVM-equivalent, ZKP, l2

Follow us: https://twitter.com/efdevcon, https://twitter.com/ethereum, https://warpcast.com/devcon
Learn more about devcon: https://www.devcon.org/
Learn more about ethereum: https://ethereum.org/ 

Visit the https://archive.devcon.org/ to gain access to the entire library of Devcon talks with the ease of filtering, playlists, personalized suggestions, decentralized access on Swarm, IPFS and more.

Devcon is the Ethereum conference for developers, researchers, thinkers, and makers. 
Devcon SEA was held in Bangkok, Thailand on Nov 12 - Nov 15, 2024.
Devcon is organized and presented by the Ethereum Foundation. To find out more, please visit https://ethereum.foundation/

## Transcript

[Music] hello everybody thank you for for coming on this Workshop we are Carlos and ignazi and we work at the protocol team at at polygon today we're going to do a workshop about optimizing the zbm throw put this is the series 2 we already did a talk about this uh a few months ago on Brussels on the FCC um but this one will be like the continuation but anyway you will can follow although you were not in the in the first one so first of all all the code that we will be explaining and using for the workshop you can get it from from this link on GitHub from the branch uh you don't really need to go during the workshop but in case after after the workshop you want to check it you should follow this okay um the beginning of the presentation will be the same as the other session we did because it's the one where it's the the part where we explain how the ZK assembly the language works we'll do an overview we explain the register the instructions we'll do some examples and we will talk about the the counters which are the the main measure to measure the performance of the zkm after that the second part this one is the one that will be different than the other presentation we will explain um two optimizations um well first we explain some new registers and new tools that we had on the Z assembly to optimize it and we will do two practical examples on we will see the code before optimizing it and after op optimizing it with the explained tools we will see the the optimization of the op code of push which is a very well I think it's the most used op code in the evm and we will also see the optimization of the ml Lo X and Ms X which are two tools that are using all along the the zbm to interact with the with the memory but first of all let's get into the ZK Assembly Language the ZK Assembly Language it's develop developing house is a language that looks like like assembly and it's used to write a program that it compiles in a way that can be fit to an Executor and after that the executor with this Trace when he's processing a state transition of a badge a bad is a group of blocks and a block is a group of transactions so it's um generating with this tra he can generate a proof of the state transition of a few transactions so this language is done like um very customized to be um the best performance on doing this the language as you can see on this image looks like this it's very similar like like assembly we will go deeper in this code um later but let me also talk you about the compiler the compiler is the one that gets the files in ZK assembly and convert it into a trace where it completely defines each one of the steps that are represented on the lines of code of the GK aam now let's check the the code um I will go to the um build file it's a Json file that is the output of the CK compiler of these lines to start with the start like seeing how the Cod how the code looks so I think I'll go maybe even bigger okay so the the CK assembly the the first threat always starts in the main CK and Fire and these are the two First Steps each one of the lines is a step as you can see here we have a resistor called a step and with this we're assigning the value of a step to a as this is the third step the step this step resistor increases by one on each one of the lines of the code so here the step value is zero and in this second line we do an assert an assert is an instructions that checks that the value in in register a is the same as the value in the left part of the line of the assert in case it's not the execution will fail and the profile cannot be generate so here what we are doing is checking that indeed we are starting the execution from the very beginning where a step is zero now let's see how it looks when it's compiled when you build the gkbm it outputs the following file it's the drum Json and it's a very long file where it has it's a Json it has a program with an array and a lot of objects in it each object sorry each object is a step let's analyze the first step following this first line where we are assigning step to a as you can see we have the in Step this this um this trace of Json is the one that um is used by the executor how so when he reads the this first object of the array he will know that he has to get as input the value of the register step and set it to a this one is the flag where it says to the executor set um the register a with the value of a step and these three lines are just metadata that is very useful for for the bagging so the representation of the first line is the representation like this when it's compiled and the same one happens with the the same thing happens with the second line This assert it represent like this first of all we have as input a constant with is which is zero and then we have the flag of assert to one It means that we have to do an assert on this step okay this was to do like a first heat up a first contact with the with the language and understand a bit how the compiler works okay now you want continue car hi okay so um what I'm hi my name is Carlos Matana I'm going to explain a little explain a little bit the language that we use the Z assembly ignasi kind of did of an introduction of how to use it and show some code examples and basically what I'm going to describe right now is the language itself I will describe all the registers that we do on the Z assembly and all the instructions that are available in order to code the Z assembly ASI said the the Z Assembly Language it's pretty much like an assembly language that uh you can you can use a standard one but uh well we have different instructions and different registers and it behaves a little bit different okay so let's first explain uh the registers we have two types of registers I would say we have long registers and a small register registers the long registers basically are registers that are composed by eight elements each one of those elements is a goldilux prime frame number and basically it's almost uh 64 bits and basically what we use it's only the less significant 32 bits if you basically join all of them together you have a register uh multiplied by 32 bits you finally have 252 bits which is the standard operations on the evm basically why are we using the goldilux prime field number basically because uh it's a zero knowledge uh friendly uh prime number so you can you can do a lot of operations in the very quickly uh when you compute uh the snacks the hashing and so on on the other side we have registers that only have one element this element is again a goldilux prime frame nbol and basically well yes it's just I would say one register that contains one element okay now that we know that we have two different types of registers let's say the the registers uh by themselves so we have five different registers we call it kind of a generic registers and they are basically a b c d and e you can do with them whatever you want we will see later on how to use them we also have a register which is uh the the state route as you may know in the zbm uh we have a spar we use an Spar Merle Tre and the summary of this Spar Merle Tre is the root and basically this uh register um is basically the state root of the state three also we have the rotat left uh C register we will know uh we will see later on how to use that uh in order to do some optimizations we'll explain later and then we have a kind of a lot of registers but those registers are one slot register uh again just one gold L Prime frame uh number and those registers are basically all the counters context St pointer pram counter gas GK pram counter return step Maxim and high po here it's important to mention I mean all of the registers um you can be familiar or not but what is important to mention that we have the pram counter in order to read smart contract bite code so we point to the smart contract bite code and then we we read the bite code and then we have the zero knowledge PR counter which is the the counter of the program itself of the ROM basically okay so we saw all the registers that we have available in the Assembly Language now we are going to see all the instructions that we have available uh that uh we can use when we code DK assembly the two first ones are I would say very typical uh in Assembly Language memory load memory store you get some value from the memory and you load it into a register or H you just save into the memory some value that it's on some register then U we support three different hash functions kak sh posidon H they'll have exactly the same structure and basically in order to do uh one of those three uh hashing uh you need to use Hash k Hash k l and hhk digest later on I will explain in details how to use those instructions but the summary could be that in the HK imagine that you have kind of a buffer empty buffer of bites and with the HK you are adding bites to that buffer with the hhk length you are basically closing that buffer and with the highest you get the result okay exactly the same for the hash P which is Hash poon and then as the other Assembly Language uh you have this kind of a jump in order to jump to another sub routine and you have kind of uh conditional jumps in in this case we have the jump negative and jump carry but they are in a sense they are basically conditional jumps okay I would say that uh the next two ones are for it's kind of a sugar syntax because um when you are developing on on Zig assembly it's very convenient uh instead of jumping to a sub routine saving the ZK program counter and when you end the sub routine jumping again to what we where you were before instead of that you you can just use this call and return instructions and I would say the the the program does it for you it automatically save the program counter and if you just uh write the instruction return it will again uh return to the previous point then we have the assert instruction just basically uh you assert that some value is exactly as other values and then the next two ones are also very important which is basically the S slot and s store and this is basically storage load and storage slot when you want to read some value from the Merkel tree from The Spar Merkel tree uh just don't how need to H use those instructions on those instructions basically I will not get into the details but basically the key Z key1 basically defines the Merle path to travel across the Merkle Tree in order to get the value and here exactly the same but you are storing some value the value will be stored on registers uh D and registers C A and B are used in order to travel across the Merkle tree then the next one is the Ari instruction this instruction is uh basically us it in order to do multiplications for um 256 bits and also in order to prove divisions you can use multiplication as well and the next ones are basically oper standard operations uh with 256 bits number which is basically addition subtraction less than signment less than equal and bitwise operations all those ones from the addition until the exor they are basically using uh what we call the binary State machine okay and this one is outdated why because uh we're going to H kind of explain um how the these instructions evolve in order to optimize some of the op codes but in order to understand it better uh we wanted to First explain uh very briefly how this old instructions uh works this is basically memory align read memory and write and memory align WR eight as you may know in ethereum uh you have three different op codes uh memory read memory WR uh 256 bits a m right eight bits that why we kind of did exactly the same instructions but on the ZK assembly uh those instructions basically works with 32 bytes but we have a special one which is the memory align right eight and this basically matches the up code of ethereum we will see later on that uh this approach was nice but in order to optimize uh the code and some op codes um we kind of uh did I would say a dynamic memory align where you can select the offset the bites that you are reading and the the basically okay now we will go a bit deeper on how the register uh works the taxonomy they are easy to use but it's true that they have some nuances that we will see now to do the zbm we have five resists a b c d and e each one of each it's an8 element goldilock Prime file number so it's 32 bits so eight elements of 32 bit it's 32 bytes to 156 bits so for example H here yes um if we have a inside we have these eight elements that we number them from A7 to a z from the most significant to least significant so regist assignment it's very simple uh you do it like this you assign five to a so in this case from A7 to A1 all the values is zero and a0 is five the same way you can assign the value of a regist to another resistor like this so after this step the value of C will be the same as a now we'll try to put a very big number in register B this is the maximum number that you can do with 32 bits in this case as 4 from B7 to B1 we will have zeros and on B 0 we will have like the maximum 32bit value which is everything fs and here is the Nuance I was talking about if we do a plus b we will be having an overflow um because this value it's not expanded to the other elements when you do it this way so in this case from B7 to B1 you will still still be having zero values and on b z you will have this value which as is a signed number it will be like negative if you really want to do um this this addition without overflowing you will have to consume a counter uh you will have to consume an arithmetic or or a yes or a binary uh counter so uh it's very important when you are developing the zbm to have this thing in mind because it can create a lot of problems you need to know that when you assign some valers to the resistors the resistors will be in at some moment greater or not than 32 bits because in case they are greater to operate with them you will have to use binaries or arithmetic instructions let's go to the next slide now I will explain more visually uh the instruction Carlos talk about uh with is the rotate LC um it's very interesting because some optimizations that we will explain later use it so I wanted to go a bit deeper on it it's very simple to understand and it's also very useful when you want to operate with 32 bits in a register but playing with the different Slots of the same registor Let Me Explain If in C we have this 32 bytes number which is very very big I have splitted them in Slots of 52 as all the elements from the element C7 to c0 so the r rotate LC will have the same value but C7 it's put in the position of c0 this way you'll see the blue one now it's the last one this is very useful when you want to apply some operation in one specific element of the register now some flowing flow control operations um I'm not going to extend a lot on it it because they are very very simple to understand Jump N when you call it um if you put the resistor that you put at the left of the instruction if it's negative you will be jumping it's like a condition instruction you will be jump will be jumping on this level but it's important to understand that it's only checking the a z op so it's only checking the last 32 bits the same happen with the jump Z zero which will jump if the value is zero jum no zero which is the opposite the jump carry it's um instruction that it's used and it will jump to the level when there is a carry on the left operation and the sh no carry it's exactly the same but the other way we have an example here of jum curry with the lower instruction so in case this condition of lower than is succeed we will be jumping on the carry level else we will be jumping on the no carry level now I will explain as I mentioned before how The Hash k hashen hash uh wor I did work I did an overview but uh basically as I mentioned with there are three instructions Hash k Hask length and Hash k the and you can imagine kind of a kind of a table okay a table where at the very beginning the buffer of bites that we are going to Hash basically it's empty and then we are going to add some bites over there so in order to add some bites into this buff we will use the instruction Hash k and this Hash k instructions is dependent on two register hash POS and D register and basically this hash POS and the registers means hash POS is basically the offset and the and D is the size so at which point do we want to add bytes and how many bytes are we going to add and then what we call the op uh which is another kind of input of the instruction it will be the bytes to add so for example in the in these three lines of code basically we're going to start at position 32 so for example this Arrow here in the middle we are going to write 32 bytes and which which bytes are we going to write basically the bytes that are in register a then we we use the Hash k instruction this e that you can see here is kind of an identifier of the hash we can have so many hashes open and we can feel a lot of bites on those hashes that are open so basically the E is kind of a an identifier of that hash okay another instruction that is very important uh not for the optimization that we're going to explain uh today but it was very important for the optimizations that we playay on Brussels is the assert the assert instructions BB of course assures you that some value must be equal to another value and this is very important the must because if an assert is triggered it means that the proof canot be generated and basically this is used to verify what we call free inputs what is a free input we have it here free input is that the pr can provide the value that the pr wants it is not checked at all so normally when we do that it's because we want the prover to provide some value but then later on we want to verify that that value is correct and this kind of opens uh some kind of optimizations that instead of computing some value you just let the pr to provide the value that the pr wants and later on you verify so not compute but verify okay so uh we talk about optimizations we talk about the language but what are we going to optimize basically what are we going to optimize are the ZK counters I did a presentation yesterday about uh the ZK count what are the ZK counters what are the ZK counters types that we have on the Z assembly and what are those numbers but very briefly you can imagine the Z counters as the gas in ethereum it's a resource that is limited on the ZK side so in ethereum you have a unit which is a block this block the resources on this block on the execution side is measured in gas and it's unid dimensional it's only the gas on the zbm the unit is the batch and this batch has some resources which are the zik couns and there are many of them so it's multi-dimensional here we can see that we have eight different resources the steps arithmetics binary memory alignment kak poson pading posidon and Shadow 56 here it's important to mention that um the numbers that you can see here are the available resources that we have in a batch for example the steps the steps are basically you have here about instead of a steps maybe you have he about uh Cycles or clocks no in other projects so here the step steps are each uh row that you hit on the ziki the in the ziki R so it's very straightforward each each line of the r it's one step so we have plenty of them because it's very I would say simple but on the other side for example the kak state machine we only have 2,000 around 2,000 uh kak State machines available why is that because the G machine is way far more complex that for example the binary State machine or the AR machine that that that's why we have way less resources of on this KY machine okay so here what we are going to talk a little bit is about all the optimizations that we did on the language itself those optimizations uh came after after a deeper analysis on what we can optimize on the r what are what is the code that is commonly used that is used everywhere that we can optimize in a very straight forward way so we realize that for example uh when we call a sub routine on the Z ZM ROM inside that sub routin we needed to use for example the the arithmetic estate machine and the arithmetic estate machine the inputs are the register a b c d and and E so what happens that when we call that function at the very beginning we need to kind of save some kind of an snapshot of the values of the registers do the functionality and then recover that value again and this is basically consuming one step two steps three steps four steps for example here in the in the example uh we are consuming four steps in order to store the value and four steps more in order to retrieve the value again so what we did on the Z assembly side uh we introduced two instructions in order for just in one step store a lot of registers just in one step so that's why we introduce these two instructions safe restore so instead of using four steps now here we are just using one step but we are calling a new instruction which is the safe and order to recover those values we are using the restore another optimization we realize that uh in there are in in the code there were a lot a lot a lot of lines of codes I would say wasted uh because we needed to somehow loading some variable from the memory into a register and then just do an addition of two registers okay this is basically consuming two steps but what if the memory State machine instead of verifying the the the O op basically the op is the I would say the the op it's all that you have on the left side I would say and so instead of verifying the op you can verify the free input the free input basically is the dollar here so this means that b basically we can join two steps into one step super easily by introducing this new instruction okay so internally in the in the in the pill we have this uh introduction of assume free flag this indicates that instead of verifying the op it verifies basically the the free input and this is this was very useful because it allow us to again reduce the steps us it in any function basically okay now will explain the memory align another optimization that we did is regarding the memine state machine as we've said the Mema line what it does is like verify the correctness of a of some bites inertion in the in the memory the main problem that we found of the first version of the Mema line is that it only supported as an input 32 bytes so what was happening is that you had like to create this in case you wanted to insert less than 32 bytes you had like to create um shifting we will see it also later like Shifting the this 32 BYT that you wanted to insert retrieving the memory that was already stored in in the memory so we add a new feature a new input to this uh um State machine which is stored in the resistor C called mode which is like coding some information that we need the M line to to have in order to operate we can add the offset bytes in case we want to insert in an offset of the slot remember that ethereum has a slot of 32 bytes this I mean this mode is coded in in a in an array of bytes so the first 64 is for the set bytes as you are playing with um two sequentially slots we can make an offset from 0 to 64 bytes the next one is the length that we want to insert into the memory and finally we can also set the if you want a left or a right alignment putting a zero or a one in position 1,192 and the same if we want little Indian or big Indian I'll show you a more deep example now so to read memory we need to read the two consecutive address of of ethereum here are represented like m0 and M1 and we need to store them in a and b and then we have the value that we want to update in the memory in the Rome we put this value in a variable called bites to store we will see it also and in C it's where we put this codification of how we want to insert this value in the memory so the first parameter it's the yes the the well the first one is the the offet the second one is the the length and the third one is the the alignment in by default the alignment is on right with the value of zero and we will be storing the value in the left of the 52 bytes slot and other way if we put the value of this alignment as one it will be stored in the left in case your St B obviously um the alignment left and right is exactly the same so it doesn't really matter and the same happen with the Indian type if we put it at zero by default we use big Indian and if we put it as one it's little Indian this is the value that it's upd dated in the memory so find um the last register is the memine right and in this case we'll have to put the original value of the memory of the first slot that you are using to a and the second one to B and the result of the memory after inserting the value to D and E and the value that um you want to insert it's in the in op in the op we will see maybe now it's a little bit difficult to imagine how it worked but we will see it in the code and it will get far easier although not that easier here um this is an example of how we've been doing the Mema line before the optimization the thing is that we wanted to read um this 32 bytes so we had to put the value of the first or the first slot that we want to read to a and the second one to B and we can choose the offset and we put it on c as you can see um here this well the symbols Mark the bytes that we want to read and we start in offset 12 as we have said here in C and when we do this mign read it will put the value of the r output on a and with this asset well this is like a test with this asset we will see that indeed the ret bway of mign read it's this 32 bytes that starts on 1 C like here 1 c and ends in like a3b a3b and more or less happens with the memine right but in this case you have to put the original value of the memory on a of the first slot and the second slot on B and the result value of the writing on D and E and doing this last last instruction what we are doing is like asserting that um indeed the stored value is this one which is 0 E1 and it starts like here also Z E1 and ends with 3 fs and ends with 3 F so this test will be they will have their correctness Pro it pro it and they will they will work another optimization is the Ari mode and this one is very useful in the ckvm for doing the this easy recover and also for doing the mul mode up code um previously of this optimization we were doing it like manually the automatization with only the add with the add machine so it really get very very very more simplified and here this is an example of another useful usage of the modular arithmetic to compute the mod the mode thanks okay so uh now let's get into the details a little bit so now we are going to see some code some J assembly but first uh I would like to explain a little bit the push up code how it works a little bit on the zbm so um as I mentioned uh we have kind of a buffer of bites this is basically um hash it and when you hash the bite code this is the the hash bite code that's introduced on the SP Merkle 3 so imagine that we have this buffer of bite okay and this buffer of bite uh it has on the on the zbm it has some limitations you can read this buffer of bites but you need to read it always in the same way so if I'm reading from bite one to bite three I always need to write those bites in the same way I mean I cannot read it again in a different way otherwise uh the proof will be broken basically so since a Vite code you need to interpret the Vite code and we don't know where the pushes are basically uh we need to write the we need to read the bite code one by one so again imagine that you have the bite code it's bite after bite okay you imagine a bite code of an a smart contract and then when you do a push operation basically you are getting a bite code from the bite code and then you are inserting into the stack so here we're reading a push one uh we are reading uh the first bite code the first bite of the the of The Bu code of the smart contract and then this will be um added into the stack basically the bite that we read is the I would say the less significant bite just for example when we have five bytes we are reading the first five bytes of a bite code in case on a on a push five and then those bytes basically are um Sav it in a register this example is basically the register will be the final value and there this final value will be saved to the stack okay this is little bit um how the push up code works on the ZM how we did kind of this performance test so basically we just kind of L some random uh bite code in this buffer of bites that represents the the hash hash smart contract P code and then we perform a lot a lot a lot a lot a lot of uh up pushes up codes okay of course we assert that the result is okay uh so for example uh here we uh test the push one basically we are starting we are starting reading the B code from position zero and we're we're reading one byte so for example here we are reading 01 okay so basically basically we are doing here an assert in order to verify that the result is correct when we call the push zero uh this case also is very straightforward we are just reading three vites from the very beginning so the very beginning is the BR counter we are reading three vites we are calling the push the push the read push basically is a generic function that reads as many bites as uh are stored in the registers d and e okay so here basically the rues should be reading 3 by 0 1 02 03 and in this case this is a push 32 if we start at the 44 4 uh2 position which is basically okay the first row are 32 byes so around here okay we're starting around here to read and how many bytes are we going to read we're going to read 30 bytes because we set here 32 in register DNA so this is the final result that we will get on the final value okay so spoiler uh the old implementation of the push um given those test consumes kind of a a lot of steps you can see that it consumes almost 6,000 steps it consumes 278 uh binary State machines and the new implementation of the push uh introduced dramatically uh the cost of the push op code as you may know the push up code is the most used op cod in in the smart contract in ethereum basically H you can see that the reduction is amazing it's it's a lot so instead of consuming 278 binaries we are consuming just only one and the steps uh we are getting a 6X basically over here credits and thanks to f a guy that is is here on the presentation uh mostly of the all of the optimizations we have thought about with him and I think that I push him a little bit in order to do those optimizations let's see some code in order to check the the push have to check it okay so let's start by reading the previous implementation of the push op code it's not easy to understand and I will not get into the details because I would rather pre I I I would rather to go into the optimization one in order to explain it how it works but basically you can this uh this push functionality in so many ways in so many ways so when we first code this functionality we were thinking to code it in a way like you code it in Rust in go or in JavaScript so you need to do some kind of looping uh some kind of for H you need to kind of shift right uh in order to accumulate the value and so on here it's not straightforward to follow again but uh basically what we uh did here is uh we have all these bites in the in the buffer of the of the hash we are divided into four bytes blocks then we need to do some operation in order to compute in order to do this kind of division and then when we have we are reading from the very end we are reading four bytes in blocks we are restoring those bites into the final value and at the very end we are reading the what we what I call here the left bytes so you can have one one block of four bytes and then one by two bytes or three bytes no so we read first the blocks we store it on the final value and then we read the left bytes and then we store it on the final value again this uh it's I mean if if you think about in in in in some standard language this is quite uh trivial and straight for what to do just do one one division one for and and that's it but when you go to the Z assembly it gets a little bit messy because as I mentioned here this first part is only the division basically here um the the RO block basically it's reading from um in blocks of four bytes and storing into the final value but in order to store into the final value without using arithmetics State machines and binary State machines we do the trick of using the rotat left but how many rotat left do I need to know well again another loop in order to rotate as much block as you have depending on the index of the block this is basically this code and when we kind of end this first Loop we enter in another loop which is the loop that uh reach the missing bites what we call uh before the left bites and then those bites also are kind of shifted again in order to to the most significant bit so here basically it's kind of a two Loops uh shifting right it's a little of messy it's very generic the code it's a little of messy so it's it it's consuming a lot of basically a lot of steps a lot of steps because every time that you do the loop even if you do in JavaScript when go and R when you do a loop you need to maintain uh the the E you need to do the the four equal to Z equal to 1 equal to two blah blah blah so here ex exactly it happens the same so you need uh to maintain our registor in order to do the loop properly check if it's zero then jump to the other loop blah blah blah so here the trick was okay let's kind of let's unroll the loop so let's not do the loop let's unroll the loop then the code looks let me find the code here so then you can see that now the optimized code of the push looks very repetitive but it's because it's because we don't need uh to do some Loops the loops are unrolled so we don't need to maintain a temporal variable in order to do again and again and again the same so it's unrolled that's why thee is very repetitive so the Cod probably gets longer but it's much much much optimized in terms of steps so let's I I will just do one example of this uh I will do two examples uh how to follow the this this function of the r push optimized R push so uh this is basically the internal the input which is d and e which is the bytes to read that's it and then the output will be in the register e then what we are doing here is okay uh we are entering into the function R push okay how many bites uh do we want to read one bite and this basically it's a jam to specific code this is a way to say that uh we want to JB to an specific line of code in this case the e as I mentioned is the number of V that we want to wrun to we want to read so depending on the E we are going to jump on a different level so if I want to read one bite I will jump to the read push one if I want to read two bite read push two and so on so imagine that we did to read just one bite then we will jump to the read push one then we assigned the program counter the pr counter you remember that is the pr counter of the smart contract bike code the very the very beginning so and then we need to read just one bite perfect remember that The Hash k instruction as I mentioned it was for writing bytes into this buffer of bytes we have the smart contract but it's also us it not just only to write but to read bite as well it is you exactly in the same way but instead of writing you're reading okay that's why here we assign the pr counter to the hash post register and then we jump to this level the r push one in the r push one basically we are as I mentioned on the hash uh K hash P or h s uh the E what we put here it's basically a pointer to the it's it's it's a it's an identifier of the hash okay and here basically we are reading one bite from the in in in in the in the bite code of the smart contract we are reading one bite and that bite we are storing it on the E and then we kind of finish the function this is quite a straightforward isn't it so imagine that we are reading bites and here it start the the tricky thing so here we are doing instead of uh jumping uh to the r push one we are jump to the r push two and here we are reading the first bite which is the most significant bite and then in the second line we are reading the second line the second uh bite this second bite since we are using the um checking the using this kind of the free input check functionality assume uh free we are loading that bite into the dollar here in the same line we are also Shifting the first value that we read the first bite H 20 and and 56 and then we are just storing that value well eight bytes and one by sorry and then we are restoring that value on E and then we are returning okay so we are reading first bite then we read the second bite and in order to store in the regist we take the the first bite we shift it and then we put here the second bite then we have kind of built the value that we return same happens with read push three and read push four and basically this uh since the most upcodes us it in ethereum is R push basically read push one um you can see that basically consumes very very very few steps then if you have if you have a push between five or 32 it B basically it jumps to a more uh generic way of reading bites and store it into the stack so for example imagine uh the example that I will explain is read push five for example in order to to make it short okay so let's uh jump to this level uh which is R push X and basically this R push X okay here as I mentioned before it's just a identifier of the H that we want to read here it kind of initialize some variables because they will be using before afterwards and then here we are basically these are I love this trick but basically we are kind of jumping in a reverse way in order to accumulate the value properly of the heights that we are reading from the buffer of uh of bites so here for example what uh in the example basically you can imagine that the E is five so the read push base table level is here is this line then we are subtracting five so we are just jumping to one two 3 4 five to this line here and here what are we doing we are reading one bite the first one here as I mentioned uh we have kind of initialized those registers so in C I will have the first bite that I have read on this buffer of bites then I will write I will read another bite remember that I have in see the first bite then in a I will accumulate this the next four byes and what we are doing here the trick is that we are shifting one slot I mean we are shifting um you remember that a register is composed by eight elements so we have shifting one element of C that remember that it has one bite we are shifting to the left and we are adding the a value and then we have the final value in order to add it to the stack you can do exactly the same exactly the same uh example but instead of five uh just imagine uh 15 16 or or whatever okay this trick is very clever because it saves it saves a lot a lot of steps of the push off code and the push off code is very us it on ethereum and basically here we are using two techniques the first one is the unrolling uh Loops so instead of doing the looping you just do all the steps that you would do in a loop so you don't need to maintain these temporary variables and then uh we use also here this uh assume free uh instruction okay okay that would be it uh want to okay so the next thing I'm I'm going to explain it's the optimization of the M load X and m x so um now we will go deeper on the code of how the state machine of the Mema line works but first before the optimizing the dis estate machine remember that I said that this estate machine only oops only was supporting as input 32 bytes so what happened is that we had like to if we wanted to store a value with less than 32 bytes we can choose the offset but we cannot choose the length so what we had to do is to read the memory that is restored start shifting um this memory with the Val that we want insert to generate this 32 by slot and then use the mign with this input here is an example of how we were doing bit before to understand the how over loading or was or doing it before the optimization imagine we want to store these bytes is two FS that in with an offset of four bytes and in the current memory we have this slot of 52 by with this all A's so at the end we want to put these two Fs in offset four so we want to put them here what we have to do is read the corrent memory read all the A's shift left and uh an amount of 32 bytes minus offset minus the length and shift right so that we had this value in a resistor after that we need to get the other side of the of the value so we all again took the current memory and we do it now the other way we shift right and then we shift left so we have this a a a and all zeros and finally we had to add the three slots so we add this one the B that we want to store the shifted right ones and the shifted left ones and we get the result and this is the input that we put to the memal line State machine luckily now it's far more easy because we can choose the length that we want to the Mema line to to prove so we just have to give him the value these two FS say the offset and say the length now let's see um in the code how was done before and how it's done now um as you can see here this is this function the M storage function it's well used among the zbm and it's used when we want to store a b of less than 32 bytes in the memory so it's using a lot of of of codes this is the not optimized code as an input we have the offset where we want to store the the value and we also have the length and previously I will not go line by line because this one is very very long but well we always check first that we have enough counters to do this function here we are storing like making a snapshot of all the resistor as you can see we are not using the save here because it's not optimized we do some checks of the memory expansion the gas if it's correct to do the this store and here we start to do jump to the m. X2 here we do these shiftings that I've shown in the in the slide it was a shift left and a shift right and here we have the value in a we shift left this value then we shift right this value then we make the addition we shift right again everything to obtain this 52 bytes that we want to insert in the memory the case is that it was very very very tricky and it had like a lot of complexity also shift right and shift left operations are very very step consuming and when we had the the we had this this value that we want to to store it's on that moment that we were doing the M align State machine here as I've said before we put the value of the first slot on a the value of the second slot on B and the resulting values on D and E here we do it with a free input and finally we do the M align State machine and with this ml Lo we are asserting that it has done correctly that indeed it's inserting the bites that we want to to insert and now we will see how is it when it is optimized you will see that the lines have been reduced dramatically we the same way check that we have enough counter as you can see we only checking steps and memine and few few very few steps in comparison with the other that in the in a first view means that really it's very very optimized comparison with the other one we do this same length check and here we use the save so instead of using I it was four or five steps for snapshotting the register we just do the save and they are all stored in just one step this function it's super super optimized actually it uses some optimization that we have not explained yet that makes it a little bit more complex to to understand but well I'll try to explain line bind line as a it's only used for trying to store memory of less than 32 bytes here we do this check in case it's more than 32 we throw an error and here in this I would say like less than 20 steps we are doing what we were doing before like in I don't know a 100 we use this free input with this zero we are sure that this free input it's less than 32 bits that it's storing in a at least this well these are like the same checks that we were doing in the other version but this one are adoped imize it also we check here that the that we're writing the memory in a lower slot than the maximum expansion bytes the maximum expansion bytes actually it's a constant we can check it yes it's a constant that is the maximum memory that you can arrive in the ckvm OR in any evm with the 30 millions of gas that you can do in a ethereum block here we do it's like an optimizing way to do the offset U that we before here having the offset we get in which slot and which offset inside the slot we are trying to insert this new value for the for the memory here we do some more checks and finally here as you can see we don't need to do any shifting because this new mine stain machine supports setting the length of the value that you want to to insert so any value lower than 32 bytes it's supported so the same way we insert the value of the first slot on a the second one of the second slot on B and by threee input we get the new value after inserting the the value that you want to insert and you do the M align State machine now this is the the new one and with this m are checking that indeed this the result that it's being inserted it's the one that we wanted to to insert so now I will I did like a test of Mema line that it runs the same code to the not optimized function and after we will run it with the optimized one m okay I have it here well this R me it's in the QR that we shared at the beginning of the presentation first we show uh we execute this test and while this test is doing some State and some mign State machines she's calling the m x and as we can see the nonoptimized one it's spending like 12 7 12 binaries 200 steps and for the new one it's only using one M line and 25 step so the optimization is huge and it really impacts a lot in how many transaction can be fit in a batch and it also this impacts a lot in the price of the transactions when you do it in the ckvm so um now this seems like little optimizations but they really make a very big impact in the pricing on the zbm so in the competitivity in the zbm with other chain so they are really really important so I think that that will be everything from our side thank you very much for coming a special thanks to F for for his contribution on the optimizing the ckvm and well hope you had out time Carlos thank you for being here [Applause] any questions yes obviously saving you're saving 10x instruction ring you know number of that by and that obviously ades across the entire supply chain from normalization aggregation basically work yes oh thank you so uh if you have kind of a push that consumes 1,000 steps and in the Prov side you need to prove 1,000 steps you will take just as an example one minute but if you need to prove 100 steps so then X less basically uh the time in order to generate that proof will be way less okay it that means that you need to rent no the infrastructure you need to rent in order toate that proof also will cost you cheaper and if it cost you cheaper it means that the impact of the layer to transaction of the user also will be cheaper also if you generate the proof much much much faster you can aggregate uh in advance of course because uh you need to when you finalize some proof then you finalize another another and then uh when all the proofs are finally uh generated then you start the aggregation phase so I mean if you finish the proofs before you will start the aggregation before as well yeah yeah of course exactly makes a big differ yeah yeah yeah yeah yeah exactly exactly so optimizing this meaning optimizing everything at at at the very end everything uh speeds up everything uh it's uh cheaper so it benefits not just only the approver the sequencer not just only the appr but also the sequencer and the users but it takes a lot of time to do those optimizations it's not easy when you analyze uh the code and the offenders and the bottlenecks and so on what do you mean I presume that you IE the codee signicant right how many how many I mean uh the r basically is an evbm implementation in zero knowledge language H we did it by hand so line by line uh how many lines I don't know more than 1,000 yes totally I think I think only the pting no well the pting I would not it's very difficult it's very diff well we have some autogenerated code of the e in the pting that it's maybe 500 lines that ector did modular exponentiation also it's crazy um more than 200 lines uh if we left kind of aside the pr compilot which you know are kind of difficult to to to do on the ZK side the rest of the code should be around to 2,000 or 3,000 lines yes you sure totally mean yeah yeah I mean for example what uh what we did is um instead of going to the code manually and check if every line and can be um optimize it what we did is basically analyze some traces of some transactions in ethereum that used to happen ethereum like a Unis swap swap uh once we got those traces we kind of buil a radio between uh how many times a op code appears on that Trace I would say a defi Trace generic defi trace and then understand the bottlenecks and then we then uh analyze the compiler and the code in order to perform better that's why we put the focus on that specific op codes because those OP codes were the op codes most used in theum most us it in uh defi uh transactions and it was they were the most offender of codes the the one that consumes a lot of steps for example and binaries and arithmetics so we decide to put the focus very focus on on on those up codes thank you for the question any more anything else yes well this basically happens on the proving side so we have kind of the main State machine the main State machine calls this secondary State machine which is the memory align State machine and in order to prove uh memory readings and writings we basically uh sort all the memory all the actions memory that uh we did and then we prove that they are correct yes yeah yeah yeah that's why if if you put all the MS stor and ml in order uh you will have the flow of an specific memory region and we are sure that it's okay on the proving side if you have more question about proving and and how the state Machin works we have here the the guys also that knows on the first yeah not if you have very easy that you're in the instruction right loading instruction initial the very first address for first the instruction Pro that's that's why I that's dirty little secret okay you're not because everything is stess except the memory memory then you need to prove that whatever manipulation happened before was done correctly so in principle so you would have to carry the proof for that particular address the last successful over and then first pro thats ver prev proof that you have anur the entire history of that addre you don't have that the that you have important that that after maybe we have like one minute for one more question maybe but very fast I think we Cann it later on we will talk later yes yeah
