Formal Verification of Execution and Consensus Clients

Originally, I started this discussion in the Eth R&D discord server:

i have a (general) question to the core devs: i think we’ve been doing a lot of progress wrt formal verification and i believe in the near future it would make sense that hard fork upgrades from clients must be formally verified before going live (this will slow down everything, yes, but i do think the price is worth it given how much we secure as a network). how do you guys think about this? (btw if this is the wrong channel, happy to move it somewhere else for discussion)

The thread generated a lot of valuable feedback already IMHO. To make the discussion more accessible and better structure the conversation around what it would take, and how we could incrementally move towards formally verifying execution and consensus clients, I created this thread here now.

Personally, I believe we have seen significant advances in formal verification recently, including the foundational work of formally specifying invariants. I would love to see client teams increasingly embrace formal verification, with the long-term goal of reaching a point where hard-fork-related client changes are formally verified before going live.

This is clearly an ambitious goal and not something we can achieve overnight. But given the value secured by the network, I believe it is worth seriously exploring what a realistic, incremental path towards that goal could look like. Please share your feedback, ideas, past experiences, and thoughts on how we can get there.

3 Likes

Thank you for starting this thread.

As you may know, many developers are working on formalizing not only cryptographic theory but also consensus algorithms, SSZ, and so on.

However, the formalization of clients is still in its early stages.
I am currently conducting formal verification on leanSpec, so please refer to that. Through formalization, I found several critical bugs and fixed leanSpec.
(Note that leanSpec itself is being actively updated.)

1 Like

>hard–fork-related client changes are formally verified before going live.
Agree with you.

I think the CL should be formalized when the Beacon spec and the Lean spec are merged. Similarly, I believe the CL and EL should be formalized, but I don’t know when that will happen.

I don’t have much to comment directly about the path toward formal proofs, I just want to make sure they aren’t relied on inappropriately as I see many people thinking that the best way to develop is to do a spec and then a formal proof and then have an AI blindly implement that spec against the formal proof. This is not the right way to utilize formal proofs IMO.

If spec, tests, or clients disagree with any of the others, then intelligent reasoning beings who know the intent should stop and analyze the situation and decide which is the correct one. We should be careful to never blindly follow any one source (even a specification or formal proof).

I think formal proofs provide a very useful additional vector for verifying correctness relative to intent, just like tests provide and multiple independent client implementations provide.

6 Likes

Thank you for starting this thread @pcaversaccio ! I agree with you that we are seeing rapid advances in formal verification. Funny enough I don’t actually think that we will need to require that clients be formally verified; once it’s possible to do so end-to-end, I think it will become so doable that it will be implicitly, socially expected due to the security guarantees associated with it. While we’re not quite at the stage where we can reasonably ask that they be end-to-end formally verified, we may arrive there much faster than we think! We are already at a place where pieces can be formally verified and I’m seeing so much new adoption that I think it may simply become standard practice .. kind of on its own.

To @MicahZoltu ‘s point about formal verification not being relied on inappropriately - we should think about formal verification is as shrinking the trusted code base, and not as something that eliminates all bugs forever. I think this framing of formal verification needs to be reinforced over and over again. There are always nuances and we should use all tools at our disposal, formal verification being part of a suite of high-assurance practices

I have been working on this, first with the consensus specs. I think a first step for @pcaversaccio original question exists today: write the specs as code that runs, pass the official tests on them, and then add proofs and test generation on top.

Etheorem is currently a Lean 4 implementation of the Ethereum consensus specs, plus supporting crypto and tooling: GitHub - etheorem/etheorem: A Lean 4 implementation of the Ethereum consensus specification for the Fulu and Gloas forks. · GitHub. The idea came from my time on the EF STEEL team. Four people work on it now, from the Ethereum Protocol Fellowship and the Invisible Garden Fellowship. SizzLean, the SSZ library inside it, has been public since June. Good to see the leanSpec work above in the same direction, and that it found critical bugs already.

What runs

Component Status
SSZ (SizzLean) Full spec type set. 2188/2188 in-scope ssz_generic vectors pass
Consensus specs State transition, fork upgrade, and fork choice for Fulu, Gloas, Heze. Official consensus-spec-tests vectors pass at both presets, pinned v1.7.0-alpha.11
Crypto FFI to OpenSSL SHA-256, blst BLS12-381, c-kzg-4844. Pure-Lean SHA-256 and Poseidon2 references beside them

What is proved

Area Done Open
SSZ 3 of 5 planned properties: roundtrip, injectivity, size bound. Over 13 constructor arms: uints, bool, bitvectors, bitlists, vectors and lists of fixed-size elements, containers with fixed or mixed fields Vectors and lists of variable-size elements; mixed containers above the offset-table limit; hashTreeRoot properties; cached tree = uncached root
Spec functions 8 of 585 characterized, 29 more touched The rest
Crypto Poseidon2 permutation = textbook reference, Mathlib proof
Trust base 3 named SHA-256 equivalence axioms

We decided to complete the executable part first and then work on the proofs. AI helps write the proofs, the Lean kernel checks the proofs, and we should do human review of the propositions. Where the Lean spec and the Python spec disagree, we record it, and a human decides which one is right.

On @dhsorens’ point about the trusted code base: the axioms above are our assumptions, and #print axioms prints everything a theorem uses.

Automatic test vectors for clients

One point I think has not been brought up is that we can potentially generate the test vectors for clients automatically from a formal model, using category-partitions, an old idea from the testing literature. I touched on this in a note I wrote on the STEEL blog: Specs as Models. The spec is the model, and the test vectors would generate automatically for all clients.

Next

  • (WIP) Automatic test vector generation from Lean 4 specs.
  • (Idea phase) Other parts of the Ethereum protocol.
  • (Idea phase) A translator from the Python specs to Lean 4, like Aeneas for Rust.
1 Like

On the execution specs side of things we have Verifereum, which is a formal model of the EVM in HOL4 maintained on the latest fork (currently Osaka) and executable (passing ~all of EEST)

Although executable, it is designed for ease of proofs and readability, not for fast execution as-is or as a complete client. One could instead use it as a reference for verifying an execution client, with one route to getting there being refining and compiling the spec itself (e.g. via CakeML).

We also have various metatheoretical properties about the EVM proved in the same repository, which are a useful basis for smart contract (application) verification on top of these semantics. And we are using this semantics as the target for verifying the Vyper compiler.

Posting the relevant references here as somehow he was not able due to restrictions:

2 Likes

To quickly chime in:

(i) Do we have authoritative formal specifications against which we could verify entire clients?
(ii) What amount of trusted code and assumptions would be required to do so?
(iii) How practical would it be to do so relative to the cadence of changes in client code? This is hard to answer relative both a lack of representative examples of this specific work and assumptions about the progress AI will make, so I’ll ignore it for now but it’s worth keeping in mind.

The answer to (i) is negative. The best set of specifications we have are the Python specifications, which are informal. We have a number of formal specifications in various languages, including, but not limited to, Lean, HOL4 (see Ramana’s post above), and Sail, but these do not cover the whole client specification, typically only the core EVM component – many of the formal specifications originate from the desire to verify smart contracts – or what is needed to specify the zkVM guest program (i.e. the Ethereum state transition function). This does not include other critical components of the clients, such as state for example, which are extremely optimised and in which bugs have previously happened.
On the consensus side, the Nyx Foundation has been working on Lean specifications for a leanEthereum consensus client but I’m not sure what the status is there.

The answer to (ii) depends heavily on the source language in which the clients are written or the possibility to verify compiled binaries for all relevant architectures (at the very least x86, aarch64, rv64im for zkVM guest programs). Let’s split this up a bit more.
First, one has to think about which representation of the code is going to be verified, anything from the high level source code, which is in principle easier to reason about but will require language specific tooling, to a compiled binary, which will be harder to reason about (also depending on the source language and associated compiler) but where the tooling would not be required to be specific to the high level language.
If we consider the languages in which clients are written, we have to look at the following languages: Go (Geth, Erigon), C# (Nethermind), Java (Besu), Rust (Reth, ethrex) on the execution, and Rust (Lighthouse, Grandine), Typescript (Lodestar), Nim (Nimbus), Go (Prysm), Java (Teku) on the consensus side. (Apologies if I’ve forgotten any relevant client.)
Rust is a highlight of this list because it both provides higher assurances out of the box as far as safe Rust is concerned and because it also has an active set of verification tools built for it (see for example hax, Aeneas, Verus, and others which are used for production code deployed at scale). These are not silver bullets: you still need specifications and, as Rust lacks any authoritative formal semantics, extracting (subsets of) Rust to a formal proof friendly representation is trusted and bugs in these extractions happen. The fact that these tools only handle a subset of Rust code also has important limitations: there is no guarantee your Rust codebase, even if it is in pure safe Rust, will extract without requiring modifications to the code. Indeed, on the cryptography side, extracting code from libraries like plonky3 and zkVM codebases has been a struggle (although it has improved recently). Whilst the Rust compiler provides memory safety assurances (which mean memory safety is typically not verified, a huge reduction in workload), it is not itself verified and compilation could change the semantics of the high level code that is verified.
The situation is worse for the other languages in that there is simply less tooling available as far as I know, the compilers offer fewer assurances, and the issue of lack of formal semantics is consistent across the board. Nethermind have proposed that there be C# semantics formalised in Lean, which would greatly help develop tooling to verify C# code in Lean, although the other issues would remain; in particular, since compiling C# down to RISC-V zkVM targets requires working around limitations of the standard .NET compiler, there is another reminder to consider the assurances we can provide about such general purpose compilers which are hard to verify. Mike Dodds has some ongoing work targetting Go but I don’t know how validated it is.
If instead we consider looking directly at binaries, which as the benefit of being a more uniform task across the range of languages used to develop clients and the possibility of providing higher assurance by removing the compiler from the trusted code base, we still find the issue of lacking specifications, including in this case with respect to the additional need for specifications of the corresponding ISA. Sail has specifications for RISC-V and x86 as well as Lean, HOL4, Rocq, and Isabelle backends (among other great tooling) but not aarch64 (Arm uses its own ASL language). ACL2 has a fantastic x86 specification (which can boot linux!), which is also the source for Sail’s x86 specification. Proofs at this level are also much harder because the representation being analysed has much less helpful structure. Brute-forcing this with AI is one option, but some first look at this (e.g. by Cody on the EF Research Engineering team) points towards limitations and token inefficiency issues; we must remember that ideally we’d want to verify several clients which all continue to evolve. Approaches based on decompilation may be helpful, and we are aware of some work towards a decompiler in Lean but have not yet evaluated it.
Another approach is to consider implementing a client in low level code, using a proof assistant like Lean as the macro assembler. This is what we’re pursuing with evm-asm which aims to implement a zkVM guest program directly in RISC-V assembly with proofs against a Lean specification and verified code generation. AI makes this possible, but development is still not free (it takes time, tokens, and consistent steering of the agents) and, as always, the issue of spec validation remains present.
One could also aim to produce a client by directly compiling from a specification (or an optimised refinement of one) via a verified compilation pipeline (see for example CertiCoq and more recently the work on Peregrine that extends this approach to other proof assistants), but we would need to assess any performance losses that happen as a result. There was some work towards this in Rocq, but I would need to get an update on it to see whether we have anything to benchmark at this point.

@alexanderlhicks

On (i), the consensus side: I can’t speak for the leanSpec status, but I can give the status of Etheorem, in the post above.

The consensus specs are written in Lean 4 in full (Fulu, Gloas and Heze0. They are executable, and they pass the official consensus-spec-tests vectors at both presets, pinned v1.7.0-alpha.11. The SSZ library underneath passes the 2188 in-scope ssz_generic vectors. What is proved so far is in the table above: the three SSZ properties, and 8 of 585 spec functions characterized.

On the spec validation point, this is the answer we practice: run the official corpus through the formal spec, and where the Lean spec and pyspec disagree, we record it and check it. Conformance to the shared corpus is what makes the formal spec more than one more opinion.

This also points to what we work on next: a formal spec validated this way can generate the test vectors for the clients, using category-partitions, the method from the note I linked above. The same model that passes the corpus can enumerate the input space and produce the vectors, so all clients are checked against it.

We are also studying a translator from the Python specs to Lean 4, codename paneas :laughing:, to keep the formal spec in sync automatically as forks change. Your point (iii) about client cadence makes a strong case to build this.

How do you FV database reads and writes in Execution Clients?