Thank you. Thank you. Cheers. Thanks. Nice to be back in a different place though, eh? Bit more full on.
Right. I've got like kind of three broad things to talk about. And I'll try and do what I don't usually do, which is leave time at the end for questions.
Right. Let's — what do you want to show on it? The screen, I guess. Mirror. All right. Good.
So I'm going to jump straight in. If you don't know, I've been — actually my talk last year at Protocolberg was discussing the protocol that I'm working on, JAM. And now we've done a lot more work on the protocol. We have some numbers and we have some further insights, and I'm going to basically go over those numbers and insights and try and give you a demo of it towards the end.
The first thing I want to cover is the security mechanism for JAM. So, if you're not familiar with JAM, it's a blockchain and its broad direction is to take a lot of the work that we did for Polkadot and combine it with some of the elements of Ethereum, particularly having a single coherent object environment.
And one of the things that we really nailed with Polkadot was getting a lot of computation through that was very secure. So we basically scaled our base of security really nicely. Now, the product that we used this scaling mechanism for is probably a bit before its time, but the underlying tech is awesome. And this underlying tech is what's being leveraged with JAM.
I want to just give you an idea of how this underlying tech works because we've seen with scaling security in protocols — we've seen a lot of work, a lot of hype, a lot of visibility go into utilizing some of the ZK tech that we've seen arrive over the last five or ten years, SNARKs and STARKs. And one thing that we've found doing our own empirical analysis of this technology is that it's shitty for scaling security. Which is why we don't use it for that. We use it for other stuff but not scaling security.
So JAM, and for what it's worth Polkadot's security mechanism, is based on ELVES. This is a protocol that was devised at the Web3 Foundation and you probably understand parts of it quite easily because parts of it take some inspiration from what Ethereum would call optimistic rollups.
But the basic problem it's trying to solve is this: everyone computes or verifies everything approach to blockchain security — to giving security to computation, giving a guarantee that the computation that has been done is indeed the correct results and of course that there won't be any alteration in the inputs, also known as finality.
And the way that we scale security is perhaps not super surprising. We use divide and conquer. You know, something that's used all over the place in computer science. In this case, we take our set of validators, say a thousand of them, and we split up the work that we want to ensure is being done correctly between these nodes, say three nodes per piece of work.
And then each of these bits of work come bundled together with a bunch of information that proves, or can prove statelessly, that the work was done correctly. And probably this is already starting to sound kind of familiar. And once we agree that each of these bundles are definitely available to any additional validator that wants to check it, then we can agree that this work is ready to be checked and after checking, ready to be finalized. And this is what we call auditing.
Now, and the way that this protocol works — the sort of clever bit of it — is how the auditing, the rules for when audits happen and who does them. These are very subtle rules and they have to be designed very carefully so that it's not possible for some attacker to get something past the auditing stage without an honest party — at least one honest party — being able to audit it with a reasonable expectation that if they audit it and it turns out to be invalid, that they broadcast it to the other honest parties.
And we have various — the ELVES protocol, the main paper, comes with various base assumptions in terms of honest parties and connectivity and all the rest of it that you would typically find in consensus protocols. And the nice thing is that the paper ultimately proves that under these rules, the likelihood of a malicious party sort of getting away, given these assumptions, is approximately the same as if the whole network itself were checking these reports.
One of the little tricks that the protocol uses is announcing before validating. And this particular trick is an important one. It's there in order to ensure that if a validator gets DoSed — basically if they get prevented, maybe there's a bug in the client — and if it is known that this validator is going to do an audit, they could potentially be DoSed, taken down, and maybe the report is going to pass without being properly audited by an honest party. Or maybe they do audit it and they get taken down afterwards or immediately on auditing in order to prevent the negative judgment from making its way to other nodes. Either way, we want to avoid this possibility.
And so what we do is we require auditors not only to self-select based on a verifiable random function but also to announce before they do anything else. And once they announce, it's almost like a contract. Any other validators that see this announcement and don't later see a positive judgment — it sets up the default. And the default is: audit it, make audits more likely for this package. Basically find other validators that will audit in the place of this missing report, of this missing judgment, for fear that there's some sort of DoS attack going on. And because this rule and a few other subtle rules, we can make a proof that this gives security approximately in line with just having all validators audit everything.
And this allows us to scale the security that we provide for our work that happens on chain. Now, if we compare this to the assumed alternative — which is basically having some effective GPU supercluster, whether it's located in one particular space or distributed around a bunch of different nodes — if we compare the cost between these two approaches, we find that this cryptoeconomic means of scaling is about two million times cheaper than an equivalent ZK proof-based approach. Not to mention latency and centralization that the ZK proof-based approach would add.
So for comparison, ELVES has a latency of basically one block, about six to ten seconds. Good luck making a serious proof of some serious computation like six seconds worth of some high-efficiency VM in that amount of time and for anywhere close to the cost profile. And if you're interested in the numbers, here's some numbers from our empirical investigations.
Basically, we can do under ELVES around almost 100 billion gas — Ethereum gas, EVM equivalent gas — per US dollar. And you can compare that to what it currently costs on two of the bigger ZK protocols, coming to around 44,000 gas per dollar, with the massive amounts of latency and finality. So this is why we're really not that bullish on using ZK methods for scaling computation specifically.
Right? That's the first part of my talk. The second part of my talk is a different thing, because I was on a plane when I authored it so I couldn't be online. And hopefully — I've never used this app before — but hopefully I can make it big. View. Play. Okay, play. There we go.
Now, this is another aspect of JAM that we've been investigating — or another aspect of building a blockchain, building a sort of smart contract general-purpose computation blockchain that we've been investigating: consensus VMs. Basically, you know about consensus VMs almost certainly. You've probably heard of — I mean you know about EVM. WASM is also used as a sort of consensus layer, as a sort of virtual machine, as an instruction set architecture. There are others. Many networks have their own, and we have one of them that's sort of ours, which is the PVM — that's a derivative of the RISC-V instruction set architecture.
And they tend to be slow, these things, right? Compared to native execution. And it's important to sort of — if we're going to design these things better, like better protocols with better consensus VMs at their base, we need to understand how they become slow in order to understand how to make them faster. And we'll assume for now that we want them faster. I mean I've heard an argument that's like, 'Ah, they don't need to be so fast. All people are ever going to do is send a balance to another smart contract.' Or another slightly better argument is, 'Ah, if there's like a really difficult piece of computation then we'll just introduce a precompile.' But the thing is, precompiles require basically governance to introduce them. And this is hard. And you also have to work out at what point to introduce a precompile and how many you introduce, given that once you've introduced it, you can't take it away. And if there's some particular computation that you want to do but that computation ends up being slightly different in the future and your precompile does the first thing but not the second thing, then you've got a big problem. You basically end up with two precompiles that do almost the same thing. It's not a great way of proceeding when our intention is to build general-purpose platforms.
So let's start the basics. Why are consensus VMs slow? Well, they have three particular — we require three particular characteristics in consensus VMs, and these are kind of slightly weird characteristics. They're not characteristics that you would typically have found in VMs pre-2004. And let's list these three things. So firstly, it has to be deterministic, right? It has to be objective and deterministic. So it's got to do the same thing for any given program plus input.
Now this seems on the face of it quite a normal thing to expect. But it turns out, not so much. Particularly for physical hardware that will tend to have a single vendor — they are not necessarily going to define everything perfectly from a mathematical point of view. Now they might want to, but there will be instances where the hardware does something different and they kind of just shrug their shoulders and say, 'Well, I'm not going to change the definition. So you just have to live with the fact that the hardware under these very particular circumstances doesn't quite do what we expect.' And this is fine in a fuzzy world where there are lots of software writers and they can just basically manage these very unlikely circumstances by turning it off and turning it on again. But it's not so great when we have a bunch of nodes that we're going to slash or somehow punish if they don't come to the same answer for any given set of inputs and program.
Now, point two: we need to be able to limit the work done. This is also a tricky thing. We need to be able to limit it also under the same circumstances. So that we either limit it or we don't limit it. Either this work has taken too long or this work didn't take too long. But we need to be able to easily hold this condition in consensus, and if it does take too long, interrupt it and then basically don't do anything.
This is similar to the third requirement, which is a much more smart-contract-oriented requirement: which is that we need to ensure that we agree on how much work was done. Now, in Ethereum terminology this would be the amount of gas that is used, right? Or the gas limit.
This is a hard one. Why is it hard? Well, because not only do we have to agree on how much work was done — and you'll find what's called metering in non-consensus VMs. For example, WASM — there will be a number of WASM implementations that will have metering. That's not so much the problem. The problem is that this metering has to be close in terms of the modeling to the amount of real-world time taken for this computation. So we might agree that some particular program took one million steps to execute, and this is information that WASM will give you if you turn on metering in one of the compliant implementations, but we don't know how much, say, an AMD Threadripper 5 GHz core — assuming that there isn't some crazy memory contention or all the rest of it — will take to execute those one million steps. The ratio is not in any way linear. Different instructions take different amounts of time, and there can be all sorts of circumstantial, situational circumstances to change the amount of execution time.
Now this is also important to understand: this isn't about trying to place a maximum bound on the execution time of a program plus input. It's really on a program trace. So it's not that we're trying to get away with solving the halting problem here, right? We totally expect that we will have to execute the program and that's fine. But what we want to do is model for any given program trace — like an execution trace, the specific instructions executed along with the state of the machine at every given instruction — we want to model how much time that takes without actually having to name a specific CPU piece of silicon, my laptop, and say, 'Well, that will do it.' And we'll have some atomic clock next to it and measure that. Because that's obviously not very practical in a decentralized network. But in a sort of decentralized sense — what in a centralized sense that's what we're trying to do, right? We have a kind of conceptual computer that's modeled, that's sort of manifested by this decentralized network. And we want a definite answer to: how much time did this non-physical machine take to execute? I mean it's kind of a mad thing to imagine but it's literally what we're trying to do here.
Now how do these requirements slow us down compared to, say, native execution? Well, the first thing is that we're not using a native ISA. Now we could use a native ISA. We could just say, right, we're going to use x86-64. Screw it. The thing is, these come with a lot of extra crap that we don't really need. And there may be some very finicky, difficult ambiguities — or worse, non-ambiguities but hardware may not implement these standards entirely correctly, and this results in a sort of difficult mapping from the ISA to native.
Now, the further away from the native hardware that we get, probably — it seems reasonable to assume — the harder it is to sort of get that native-level speed, assuming that we want the same generality. If we're only trying to do one thing, like I don't know, send a transaction or compute a Fourier transform, then it's fine and we just make a totally optimal version of that and then bam. Our sort of instruction set — which is presumably just FFT with the input — doesn't need to be anything like the native hardware on which it's running because it's only got to do one thing. But if we want something that's true and complete — something that's actually general — then it's presumably going to be a lot faster to make it closer to the actual native ISA that this thing is going to be running on. And we already can see this — like it's theory, but we can demonstrate this. We have evidence.
And the next thing that's hard is that the wall-clock execution time is not easily predictable. Now, it probably used to be quite easily predictable like long ago in the 486 era, and may even still be relatively easily predictable on some types of hardware now — simple hardware basically. But if we want this stuff to run fast on modern CPUs, on server or consumer-grade CPUs that we would find in this laptop or that laptop or some server somewhere, then things become a lot harder.
And what we're left with is kind of two paths for doing this. One of them — and they're kind of interesting paths philosophically because they trade off accuracy for precision. Two words that a lot of people often confuse or mix up or think are synonymous. They're not synonymous. And this subtlety is actually here. One of them is a theoretical estimation. This is like gas metering. This is where we say: right, it started with this input, it did this instruction, this instruction, this instruction. And we think it took 15 gas, which equates to, I don't know, 15 nanoseconds. Very objective, totally precise, but unfortunately wildly inaccurate for basically any real-world hardware.
The alternative is to get something that's much more accurate — i.e. closer to reality — but unfortunately subjective. And this is like: run it on your own machine. Run it on standard hardware. Get a number. It's probably going to be close to the number that anyone else gets when they run it on their own version of the standard hardware. It won't be exactly the same, but as long as you take proper precautions, it shouldn't be too far off. And so we have this precise-but-inaccurate versus accurate-but-imprecise. And it's not always so easy to know which way to go down.
For what it's worth, Polkadot at the moment — the relay chain of Polkadot, not the smart contract parachains, but the relay chain — uses the second method, whereas most smart contract chains pretty much use the first method. And that's kind of how Polkadot gets a lot more processing done. But it does mean that there is in some sense less granularity. You kind of can't combine different users of the computational resource in the same invocation. Essentially that's what it comes down to.
So it's interesting — we're interested to know how bad this really is. There are a few different ways that modern CPUs speed things up and do so in some opaque way that's not actually got anything to do with the instruction. Caching is a really big obvious one, but there are a few others: branch prediction, pipelining, and so on. There are a lot of tricks that they use and these tricks are very much — firstly they're CPU vendor specific. They might even be CPU model specific, and they are designed to be opaque. They're designed so that the software can't really tell what's going on.
Now, there's probably some cunning tradeoff between making the consensus-level instruction set architecture be kind of closer to native and further from typical usage. We would assume that the closer it is to native, the faster we can make it go. But the further it is away from typical usage, the harder the cost model, it turns out. But assuming that we want long-term generality, we kind of have to be quite close to native because we need the flexibility. We can't just assume that we know ahead of time precisely which operations the user — or which compound operations, FFTs or matrix multiplications or whatever — the user wants to do.
All right. So let's take it to the numbers. We came up with a couple of experiments. And we have at the bottom there our sort of platform that we're running computation on. We run it on a native x64 platform, some standard hardware. We also run it under our PolkaVM — our interpreter. We actually have both an interpreter, which sort of goes through and interprets every instruction one at a time, and also a recompiler, which basically takes the whole program code and recompiles it from the instruction set of PVM into native x64, and then runs it as an x64 program — a bit like JVM with the JIT, except this isn't JIT, it's ahead of time. So it actually goes through the whole thing and streams out a native program and then runs that.
And then on top of here — on top of this platform, whether it's native, an interpreter, or a recompiler — we run something useful, something vaguely useful. So one of them is a SHA1 hash of 100 kilobytes of data. Another one is this mad interpreter inception thing, where we actually run a RISC-V interpreter, and then inside of the RISC-V interpreter a 6502 interpreter — actually a whole NES emulator. But a big part of that is the 6502 CPU. And then on top of that, basically one of these hardware test ROMs that just goes through and tests all the hardware.
And the idea of this is to make something that isn't particularly well suited to anything. It's largely unopinionated — or the intention anyway is a largely unopinionated amount of computation that's probably going to be quite pessimistic for our gas model, simply because it involves a lot of arbitrary memory reads, and we found that arbitrary memory reads are hard. So we wanted something that was kind of pretty hard to do. We also wanted something that we could recompile with Solidity and make a RISC-V interpreter. Turns out it's actually quite easy to make a RISC-V interpreter in Solidity, compile it to EVM, and this gave us a really nice way of having pretty hardcore compute but comparing it across the board.
Now, for the hashing, we used a hand-optimized assembly version of an EVM SHA1 hasher, and we compared this to just compiling the standard SHA1 crate in Rust to either native x64 or PVM. So again, we tried to make things hard for ourselves and we tried to give EVM as good a break as it could get. Also, for example, we switched off the bounds checking for the RISC-V interpreter running on EVM in order to try to give it as good a break as possible.
Now, we had three gas cost models, because at the end of the day — it's all very well metering what instructions pass, and we can even meter different instructions differently. A multiply or a divide probably takes more time than an addition. But for memory instructions, it's massively dependent on whether the thing you want to load from memory is in the cache or not. And broadly speaking, you've got four possibilities with modern hardware: it's either going to be in L1, it's going to be in L2, it's going to be in L3, or it's not going to be in any of them and you have to go to main memory. And if you choose the right CPU, you'll get about one megabyte of L2. That's about as big as it gets on the consumer side, but still about one megabyte of L2 per core. L3 it can go into the ten-plus megabytes, and then after that it's like main memory.
And what we wanted to do was take the ratio of gas to actual wall-clock execution time, depending on whether we assume it will be — we didn't bother assuming it's an L1 hit because it's trivial to write code that can prevent L1 hits, right? It's trivial to write malicious code. But we did want to compare it for L2 hits, L3 hits, and an L3 miss. And we wanted also to compare the difference between recompilation onto native, interpretation on native, and just running it straight to native — like compiling to native directly and running it as native.
And these are the numbers. So basically, running as native gets us — actually, I think this number is slightly different, that should be 0.052 instead of 0.14 — we managed to get that down to 0.075. But yeah, so it's not too far off after we managed to get the right code running. The native is about half as fast as recompilation. Actually, recompilation is a little bit better in terms of ratio. Interpreter quite a lot slower. And then depending on our estimation — if we make an assumption that we will always hit the L2 cache, which can probably be engineered by a combination of limiting the amount of accessible memory and using a memory manager that basically randomizes things on each of the different nodes so that it's impossible to predict where two different memory pages are going to be in physical RAM. But anyway, this is sort of CPU stuff, but the point...
Is that it's not unreasonable that we could expect that we can guarantee L2 misses under circumstances that are still usable. For programs that need more memory or whatever, then the L3 hit would be the number to take. And then for programs that, you know, just need a gigabyte and it's RAM.
That's the interpreter inception, the hashing, the char1 hash — so similar, similar kind of numbers, a bit smaller, about a thousand times smaller.
So how do we get these numbers as small as possible? Because obviously we want to make our estimations be as close to wall clock reality time as possible. They're close. They're never going to be lower than, or they should never be lower than wall clock reality time, because that would imply that we are effectively giving more work than the network could possibly do. But they should be as close as possible to it. And at the moment there may be an order of magnitude, one to two orders of magnitude off, assuming that we recompile.
So how do we get it closer? Well, one of them, as I mentioned, is to make some assumptions on memory usage. Basically make some assumptions that the L2 cache is always going to get hit. And the way that you do this in our sort of experiments — and we're fairly confident that this is a viable strategy — is to limit the amount of memory available and do some cunning page mapping magic, ensuring that they are at unpredictable places in physical memory and therefore that guest code that's running cannot know which bytes it should be hitting in order to cause collisions on cache lines.
But there's another thing that we can do. Economically sound augmented metering is what I've very quickly named it. And this helps us make up that gap a lot further. Essentially what we want to do is apply a metering factor, right? So we want to say — we typically, non-malicious code is not going to run at the speed that we assume malicious. This is kind of assuming it's malicious. And normal code most of the time isn't going to be malicious. We cannot assume that it's not malicious as long as anyone can run any code by paying some baseline amount. But what if they didn't just pay a baseline amount? What if they put some sort of stake in the system such that they lost their stake if the code turned out to be maliciously engineered and takes a lot longer than the lower estimates, the lower end, the second line down basically.
And this becomes like an economic answer to the problem. Essentially we keep our objectiveness because we have an objective ratio that we arrive at through economic means and we have an objective gas metering system that we arrive at through purely computational means, and we combine them together to get a final objective time. And this makes up for the fact that we can't get close enough in a purely computational fashion. And this is not unexpected. Hardware, CPU hardware is complicated AF. Pretending that we can write software that can understand what the CPU is doing under the hood and come up with a number that reflects the time that it will take or that it has taken is unrealistic. And therefore our only reasonable course of action is to move into the economic domain where we can reasonably believe that if someone hasn't written code specifically to break our gas metering system, then they can probably be sure it's going to be not far off the average sort of expected time. And you know, if they care, they can measure it themselves under the input or class of inputs that they expect and become more confident.
So we need two things basically. We need a means of disincentivizing malicious behavior, disincentivizing the engineering of programs that take longer than our gas metering plus ratio would suggest, and the means of working out what ratios will ultimately be able to be considered valid under consensus. So we have to get some agreement on what ratio is valid for any given program execution.
Now it's easy enough to form a disincentive, right? You take a stake and you just say we'll slash it if we agree that the program took too long to execute. But how do we decide that it took too long to execute? And furthermore, given that we then have a method for deciding, how do we ensure that whoever is going to be staking can be confident in that answer, in that solution, in that decision? We want to make sure that the judges are not going to be biased against you if you're putting your money on the line. So basically it comes down to three parts: subjective measurement, which is where the staker themselves actually measures how long it takes for them on their instance of the standard hardware; statistical consolidation, where those judges, the validators in essence, measure how long it takes on each of their instances of the standard hardware and then statistical consolidation of that into a single value; and thresholding — and in principle it doesn't need to be consolidated into a single value, it could be consolidated into some sort of distribution and thresholding applied to that. And then we come up with basically a confidence bound as to how likely it was that the ratio was intentionally breached. And if it was very likely intentionally breached — basically because everyone agrees that the real ratio is far worse than the stake ratio — then we slash more. And if it's not particularly obvious it was intentionally breached but it was probably breached a bit, then we slash less.
Now this mechanism, in a lesser sort of imagining, is already practically deployed and has been deployed for years in Polkadot. And this would represent a slightly more sophisticated reimagining of it that would give one very particularly useful thing, which is objectivity over the amount of gas used. And this is because we have the linear relationship now, because we can take our ratio — which we know is going to be sound because of this incentive structure — and our gas metering, which we know is objective anyway, and combine the two. On Polkadot we can't do that.
So let's compare, let's see what this gains us. Let's compare it to a couple of implementations of Ethereum nodes and EVM. So you have a couple of implementations there, and we're comparing this to our PVM essentially, our JAM implementation, but our PolkaVM implementation for PVM. Now this is for the interpreter inception. There are similar numbers for hashing. So the native wall clock time is the amount of time that it took for this to run when it was compiled to native. Right? So we took our RISC-V interpreter, compiled that to native, ran our 6502 interpreter, our NES emulator on top of that, within that as a guest program. And then we ran the NES test ROM inside of that as a guest program. And we came out — we ran it for a frame or two, a few frames — and it took half a second to execute.
Okay, now we move into the consensus VMs. So instead of compiling our RISC-V interpreter to native and running that with the guest, we compile it to — we make a RISC-V implementation ported to Solidity and compile it into EVM and we run it on one of the two Ethereum EVM environments, implementations. Now, they both obviously reported the same gas taken, so fine. It's not that relevant at this point because it doesn't obviously correspond to any particular time taken. I mean, once it did when we were figuring it all out in 2015, trying to work out how much gas represented, you know, a second of computation, but it doesn't really make any sense anymore. And then we also check to see how long it took, like wall clock time, to execute the same workload.
So what took 0.52 seconds natively took 654 seconds when ported — ported reasonably well mind, to Solidity, not like some dodgy highly pessimistic port, we did our best to actually make it run fast — and 102 seconds on, how do you pronounce that, EthOne. And then if we work it through, the amounts of blocks that this would take to actually do this workload on an Ethereum chain with similar parameters to the Ethereum L1, then it would take about four and a half thousand blocks, which again with similar parameters to ETH L1 will take about five and a half thousand seconds.
The slowdown therefore, so that it could be attributable to the change in ISA, the change in machine spec, is about 200x for EthOne and about 1250x for Geth. So Geth is quite a lot slower, right? It introduced a much greater slowdown, presumably because its EVM implementation isn't as fast.
The amount of slowdown we can attribute specifically to the protocol. So given an EVM implementation's speed of execution for this workload, how much longer does it take the protocol to get through the workload? Well, Geth does better on this because of course it slowed down a lot more at the ISA stage. Yeah, its EVM just basically runs stuff slower. So it doesn't seem as bad that it takes longer for Ethereum to actually process it on chain. And that can be attributed to about 85x for Geth and about 40x for EthOne. And then if you multiply these together, you end up with the same figure, the total slowdown, which is about 100,000. So it takes about 100,000 times longer to do computation on Ethereum than it does to do the same computation natively.
So let's compare this to PVM and see how much better we can get. So again, the native is the same. It doesn't make any difference here. Now we have a gas — now the gas in PVM spec is specified as being the maximum worst case time taken for execution. Yeah. So the maximum time taken, maximum worst case, worst case time, maximum time for execution. In this case, PolkaVM alone, without this additional sound or augmented metering, is 18.7, right? This assumes an L2 hit.
So it takes, whatever that works out as, 35x-ish as much time, or it's 35x worse, our objective estimation compared to native. Now how long did it actually take to execute? 8 seconds, right? So actually not far off native, like really quite close to native. Fairly impressive we would say. But unfortunately this proximity to native is only half of the solution. We need to ensure that worst case, we can predict our worst case estimation is also close to native. And in this case it's hard.
So our effective VM slowdown from this is — oh, that should not say oh, no, yeah — our effective VM slowdown for this is one and a half, right? Because our VM takes about one and a halfx longer than running it natively. This is pretty good, right? This is pretty good. But how does this work in terms of protocol when the gas model kicks in and the block limits kick in? Now for block limits it's actually pretty good because in JAM all the work is done essentially asynchronously by the worker nodes, by the sort of validator nodes. And therefore the amount of work that validators can sort of get through is the same as pretty much, like the same as the wall clock time, as long as the gas mechanism is sound, right? So it has to be sound but because it's sound it can be the same. We don't need to account for a bunch of the other stuff that would result in most chains having lots of space, doing a lot less compute even in terms of perfect gas timing, because all of the JAM work is sort of done asynchronously, pipelined, there's a lot of sort of optimizations there.
So let's look at the protocol, let's look at the gas time. So our gas time is around 18.7. So our sort of best estimation that is sound, that is definitely not attackable, is secure, is 18.7. In reality it only took 0.5, but our best secure guess is 18.7. And therefore that's where we get the bulk of our protocol slowdown from. In fact there's a little bit more than that because our VM did take longer than native, but only a little bit longer than native. So it ends up being, for the protocol slowdown portion, only about 23x, and we end up with a total slowdown of 36x for the sort of full, after all is considered.
Now if we introduce this sort of economic mechanism for bringing that ratio down — which assumes additional stake, right? So it's probably the person who is uploading the code or providing the inputs is going to be somewhat equivocal on how much stake they want to put in, like how much they want to bet the ratio is better. But in principle this can be reduced to a very, very small factor slowdown. In principle, you know, two and a halfx slowdown, which would be pretty good. This would allow for basically two and a halfx native — or whatever, 40%, 30, 35, 40% native speed.
The numbers for the hashing are a little different but not too far off, and we see sort of similar — hashing is actually a bit of a harder task it turns out. So for the EVM, Ethereum side it's more like 300,000, and for the PVM side the best case is closer to four and a halfx. Good. That is the consensus pessimizations part.
Now I'm going to skip back to the other bit, the other slideshow. Right, I want to talk a little bit about core VM. How much time do I have? Not so much. Right, I'm going to really go through this fast because I want to — so JAM is progressing. We have tooling. Here's lots of screenshots of tooling.
There's like — JAM breaks down into having two bits of logic for its smart contract, if you like. One of them is a really fast pre-processor that we call refining, and the other one is a still quite fast in-consensus processor called accumulation. Basically we do lots of refining in parallel, high performance, every block, and then all of that comes together kind of fan-in and gets all combined as needed in accumulate.
Good. Core VM is a means of using this. How does it use this? It basically allows in the refine stage — this very high performance but largely stateless stage — it allows a virtual machine instance, basically a guest instance of a very regular virtual machine, PVM. So if you're not familiar with PVM, you're probably not. It's very different to EVM in that it's not stack-based, it's register-based. It has regular memory, 32-bit addressable. And it's metered and it's fast, like I just sort of mentioned.
You can compile regular code to it, right? So it turns out that because it's RISC-V compatible — so it's basically a derived ISA from RISC-V — when you actually build code, you build it to RISC-V and then you just run a sort of a Swift one-pass recompiler and it turns it from RISC-V into PVM. PVM is essentially a pre-processing step that makes it a lot easier and more optimal to then recompile it further into x64. So we're kind of using RISC-V and PVM as intermediate targets that are good for consensus code, good for gas metering, but not as bulky and potentially ambiguous as straight x64.
And the great thing about having guest VMs — at least ones that you can read the memory of, sort of peek and poke into the memory — is that we can pack up the VM execution and then start it again in the next block in some, you know, stateless way such that the guest of that virtual machine doesn't realize it has been stopped and restarted. This gives us the apparent continuous execution for these guests. So you can think of core VM as essentially a sort of Docker container for regular software, a regular machine.
How does it work? Well, basically it sort of initializes its registers to like zero, its program counter to zero, empty page table, memory is empty. It sets the code — it's a Harvard architecture machine, so the code sits outside of memory — and then it basically just advances it, either just doing regular computation if it doesn't need to read anything from memory. If it gets out of gas, it basically just snapshots all of its memory and sticks it in the data lake, a big data availability system that JAM provides. If it's a page fault, it tries to fetch the data from the data lake or introduce a zero page in the table if there isn't already a page in that place in memory. And there are host calls that allow it to output data basically to — in a sort of I don't know, kind of output like graphics card, sound card, kind of like beyond the machine, some destination for data beyond the machine — and we provide this through a particular host call, almost like an operating system call.
And I'm not going to go into the architecture particularly, I don't really have time. So there's a bunch of tools — a tool for monitoring these output streams, so virtual hardware, a virtual monitor, virtual speakers, etc., virtual console — because of course we want regular software to run in this — and a builder which basically just works out, is a memory manager basically, it works out which memory pages are needed for any given sort of execution slice of the machine.
Right, let's see if I can make this work. I sort of have — I thought everything was already set up from the last presentation I gave, and then I turned the laptop on just to make sure that it was all ready and waiting, and then it turned out that I had installed a different distribution of Linux and didn't really have — well, didn't have any kind of setup on it. So I very quickly, 15 minutes before coming to this talk, tried to set everything up again, which took — I haven't had time to actually test it. So it may or may not work. I mean, I don't even — oh wow. It's actually — is that — no, I need to mirror it. How do I mirror on here? Settings. Settings. Which of these is settings? Application settings. Ah, here we go. Settings. Displays. External display mirror. Mirror. Does that work? Yes. All right. Going.
Right. Now, so I'm going to start a test net. No, I'm not. Oh dear. This doesn't usually break. What is going on? Ah, yes. Okay. Now I see. I just have to run this mad line here, and probably I need to sudo it, and then run the test net.
Okay, so the test net is sort of started and it's doing a thing. What this has done is spun up six nodes on my laptop, and then each node is running — is a JAM node. So they're all sort of talking to each other. And then I've got this program called jamtop, which is a bit like top — I don't know if you know top, like it's a Unix utility. It shows you what's going on on your machine. In this case, there is only one thing going on on the JAM machine, which is this bootstrap service. The bootstrap service is basically like a BIOS or whatever, like a firmware that lets you load other software onto JAM and kick it off.
Now, the first thing I need to do is load some data onto JAM, which I'm going to do — this. This starts — oh dear, battery is low. — this starts a virtual file system on JAM, on the blockchain, and uploads some data to it. It takes a little bit of data, so it's going to spend a little bit of time uploading. And you can see the upload happening basically with this P flag here. I don't know if you can see that. No, you can't. It's really small. I'm not sure why it's so small. Maybe I can make it bigger.
Bigger. How do I make it bigger? 14-inch laptop display that's like projected to — resolution. Yeah. Don't know — anyone know how to use this like Cosmic thing? No. All right. Never mind.
Anyway, so that has been now uploaded. You can just see that's finished there. And what we're going to do next is make a new VM. I'm going to copy from my cheat sheet. Now what this command does — you can't really see it, I'm sure — it starts a new VM using this JAM tool, JAMT, which connects to the network, just to one of the — it actually spins up its own node. The node connects to the network. It introduces a new process into JAM, and then that process is this core VM process, this kind of Docker if you like. And then it immediately takes that process, once it's been created, that sort of executable, and tells it I want you to start this other guest executable inside of your container. Yeah. So it's a bit like saying Docker — I don't know, with a Linux thing — and then start, I don't know, Apache. Right? It's a bit like doing that in DevOps terms.
Let's see if I can make this at least a bit bigger. Control-plus. I'm on Mac usually so this is — huh. Made it bigger for me. Oh nice, okay cool. Can I make it brighter? No, I don't know. Don't want to jinx it. Maybe they can make it brighter. Maybe the gods of the projector can make it brighter.
Okay. So that's completed, right? So what that's done is it's just basically, as I said, created this kind of VM container and then initialized it with a program. And we get given the service ID. This is just — this is like a process ID, right? So if you're on Linux or Unix, all the processes that are running will have an ID. It's like a number. And this gives you the same thing. You can see it down here. You can see the same thing up here on jamtop, right? It's the same thing. And if you — I don't know if you can see it, but it tells you on jamtop, right? There's a VM. This service is a VM, and the thing running in it — well, that's this thing here.
So the next thing that we need to do is we need to connect the output of this Docker to an actual monitor, right? To something so we can see what's going on inside of the Docker container basically. And that's what we're doing here. And in order for this guy to sort of know which VM it should be connecting the monitor into, we need to give it this ID, right? This process ID, this service ID. So that's what I've just done there. And then what I'm doing here is I'm telling it to start actually ticking it along. Yeah, because someone's going to have to sort of pay or whatever for this thing to run. It's not free.
Network is unreachable. Right, this I remember this program from before. And this is because I don't have WiFi. So let me put WiFi on. Do we have a — I just use my phone. Right, now when you're doing personal hotspots you need to go into the settings and like flick this personal hotspot thing and eventually it should turn up, right? I mean, maybe — there we are. Okay. Connect. Connect. How do I connect? There we go. Is that going to work?
[Music] Thinking about it. Still thinking.
Okay. Do we have a password for the Protocol Berg network or any of these really? Is that a — oh, Department of Decentralization. Okay. I don't know that then. Department of Decentralization. Okay.
Right.
[Music] Okay. Is that going to work? It might have worked. Looks — yeah. Let's see. No. Network is still unreachable. Does that mean it's connected? No.
Right. Okay. So, we have the monitor there. We have this there. Yeah, it's got this dot dot dot thing going on, which I'm not confident means connected. I'm going to — okay, right, I'm going to turn it off and on again. And hopefully — all right, it's off. Airplane mode on, right? Airplane mode off. WiFi on.
Department of Decentralization. Why is this white thing here? It's not doing anything. I've got a feeling that it's like breaking at the protocol. Okay.
Right. Well, I'm afraid you're going to be denied the cool thing, but what it was going to do is send a bunch of — or send work packages into the JAM network which ticked forward this virtual machine, and that would have resulted in output going into the virtual monitor, which was monitored by our monitor program here, which would have made a window and displayed what was going on in the machine. And if you could read that there — which you can't really — it would have displayed the game Quake proceeding. Good. Well, not really, but — no, it's definitely not happy with the network. Good. Right, that's about all I came to say.