Fault Proofs in the OP Stack
Base recently launched fault proofs on mainnet, marking a significant milestone in our journey towards progressive decentralization. Fault proofs allow anyone to participate in securing Base, creating a more trusted and open economy. A core property of fault proofs is that if there’s at least one honest participant, or “challenger” engaging in the fault proof game, then the game should resolve correctly. They are a critical component of the Base stack, and as we continue to increase throughput on Base, we need to ensure that our scaling efforts do not introduce any incompatibilities with fault proofs. Therefore, we launched a dedicated initiative aimed at analyzing how the Fault Proof System in the OP Stack would be impacted by our scaling efforts, and ensuring that we can continue to scale fault proofs safely. Today, Base’s Fault Proof System leverages the Cannon Fault Proof Virtual Machine (FPVM). You can read more about fault proofs in the OP Stack here.
Cannon is the first Fault Proof VM (FPVM) supported by the OP Stack. It combines onchain and offchain components in order to effectively run the EVM within the EVM, ensuring that any state transition can be audited, and permissionlessly challenged. Unlike typical Ethereum implementations which only ensure that a transaction has a deterministic output (e.g. balances and token transfers), the Cannon FPVM emulates a computer which behaves deterministically - down to the individual MIPS instruction - all while running its own instance of the EVM.
Identifying the constraints
Given the complexity associated with fault proofs, today’s Cannon implementation contains certain simplifications. Cannon implements a minimal set of unix system calls - just enough to allocate memory, read and write to a preimage oracle, and exit gracefully. Notably, this excludes syscalls related to creating and managing threads, or other concurrency primitives. As Cannon is written in Go, a garbage-collected language optimized for concurrent code, this has a few drawbacks. Most importantly, Cannon is unable to clean up memory once it’s no longer needed, causing the footprint to grow until either the program completes or it hits the hard limit imposed by the 32-bit MIPS architecture. This means that each Cannon execution has a hard constraint on the available memory. Because the execution behavior is entirely deterministic (an essential property of a Fault Proof System), any input which causes Cannon to run out of memory will always cause it to fail.
Cannon isn’t only racing against its memory limit, but also against the clock. Base provides a set window of time for anyone to verify or challenge the state published back to L1. In a fault proof dispute game with two players, each player has just over 3 days to run Cannon from start to finish in order to participate. Because Cannon provides several layers of virtual machine abstraction, and optimizes for determinism, block execution is many orders of magnitude slower on Cannon than on a standard Base node.
In order to safely ship fault proofs to mainnet with these restrictions, we needed to ensure that all blocks would always be provable - especially as we raise our gas target. In other words, no combination of blocks could “break” Cannon by taking too much time or memory. This of course meant we needed to stress-test the system in as many ways as possible.
Introducing new tooling
To address this challenge, we built an open source tool to generate repeatable fault proof test cases within a local devnet. We first generated and executed these tests outside of Cannon to develop and iterate more quickly, and then ran them in the slower Cannon environment for comparison and metrics collection.
Each test scenario consisted of a single transaction configured with a given gas target. This transaction would deploy a contract that would repeatedly perform a specific operation pattern until the gas target was met or surpassed. Each contract and associated forge script is defined in the testing repository, linked above. These test scenarios were executed at 4 gas targets: 1Mgas, 2Mgas, 3Mgas, and 20Mgas, in order to produce and validate a linear scaling model, which then allowed us to forecast the impact of a block containing 100Mgas with the same characteristics. Test coverage included ETH Transfers, ERC20 Transfers, Storage Writes, Storage Reads, Contract Deploys, as well as the majority of precompiled contracts. We omitted precompiles that do not pose a risk to resource constraints in Cannon, such as those which can be offloaded to the L1 EVM.
For each test case, we specifically evaluated the Cannon MIPS instruction count, memory usage, and wall clock runtime. We performed these tests on a less-performant runtime environment than our production challenger. From this baseline we again rounded down to an extremely conservative 10M instructions per second execution speed, in order to forecast the worst case challenger proof time for each test fixture.
What we learned
From these tests we identified two specific usage patterns which stood out the most in terms of resource constraints, the p256Verify and ECAdd precompiles, which are detailed below. Importantly, we validated that these constraints will not limit our scaling efforts below 60Mgas/sec.
p256Verify: Time intensive
The p256Verify precompile (introduced in RIP-7212) consumes the largest amount of Cannon instructions per gas unit, and cannot be simply accelerated in the onchain MIPS.sol contract using the existing L1 precompile preimage execution pattern, as it is not yet deployed on L1. At around 10,000 Cannon instructions per gas, and using an extremely conservative execution estimate of 10,000,000 instructions per second, the p256Verify precompile can consume around 1ms per gas. At a much higher block gas limit of 100Mgas, execution may take as long as 28 hours with these performance characteristics. Fortunately, this is still well under the challenge timeout of ~3.5 days, and minor improvements to the challenger’s setup, such as faster hardware or software optimizations can significantly reduce this duration.
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 17,049,500,014 | 131.29 MB | 1,277,339 |
2Mgas | 2,000,000 | 27,803,520,661 | 129.99 MB | 1,878,936 |
3Mgas | 3,000,000 | 38,780,421,364 | 133.84 MB | 2,585,735 |
20Mgas | 20,000,000 | 210,254,748,805 | 155.70 MB | 13,078,063 |
Estimates | ||||
20Mgas | 20,000,000 | 223,456,106,163 | 154.60 MB | 13,689,567 |
100Mgas | 100,000,000 | 1,224,139,467,745 | 288.95 MB | 75,132,093 |
p256Verify: Memory usage and runtime in ms given actual and estimated gas usage
ECAdd: Memory intensive
The ECAdd precompile shows the largest rate of memory consumption with respect to gas usage, at around 7 bytes per gas. Verifying a 20Mgas block primarily consisting of ECAdd precompiles required roughly 250MB of Cannon memory (inclusive of ~135MB of L1 block overhead), suggesting that a 100Mgas block following this pattern may use roughly 690MB of memory. This is still well below the 32bit MIPS Cannon heap limit of ~1.1GB, and will allow for up to a 2x margin of error on top of typical consumption from real-world L1 blocks.
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 7,018,067,400 | 143.96 MB | 549,523 |
2Mgas | 2,000,000 | 7,614,519,602 | 150.25 MB | 584,388 |
3Mgas | 3,000,000 | 8,483,630,982 | 162.28 MB | 643,260 |
20Mgas | 20,000,000 | 15,553,134,512 | 250.38 MB | 1,241,447 |
Estimates | ||||
20Mgas | 20,000,000 | 20,895,478,233 | 317.06 MB | 1,436,023 |
100Mgas | 100,000,000 | 50,658,462,679 | 689.75 MB | 4,134,394 |
ECAdd: Memory usage and runtime in ms given actual and estimated gas usage
Instruction count, memory usage, and wall clock runtime were not materially increased by modest increases in gas consumption attributed to accelerated precompiles, storage reads/writes, contract deploys, or transfers of ETH or simple ERC-20 tokens. Other non-accelerated precompiles such as SHA256 and ECMul consumed relatively larger amounts of instructions and memory as compared to simple EVM bytecode, but not enough to pose any risks to the integrity of the fault dispute game, even at greatly increased block gas limits.
Span batch implications
One more important finding is that Cannon’s resource usage is not only influenced by the contents of a single L2 block being validated, but also by the contents of all L2 blocks contained within the same span batch. This means that even if a single block might need to contain ~200Mgas of ECAdd precompiles in order to break Cannon, far exceeding today’s gas limit, a span batch containing 10 blocks can easily surpass this with room to spare. To mitigate this risk, we’re setting an extremely conservative span batch gas limit of 120Mgas for Base mainnet, and limiting the number of L2 blocks per batch based on the per-block gas limit.
Looking ahead
With Base building one block every 2 second, and at a limit of 120Mgas per block, we can scale to roughly 60Mgas/sec with the current version of Cannon. This is roughly a 3x increase from where we are today, but far below our north star goal of 1Gigagas/sec. Fortunately there are a number of workstreams in progress which will help to scale fault proofs alongside our goals.
OP Stack contributors are developing two major upgrades to Cannon which will address the memory constraints, on top of plenty of smaller improvements and optimizations: Multithreaded (MT) and 64bit Cannon. MT Cannon will introduce runtime garbage collection, preventing the memory usage from growing in an unbounded fashion. Additionally, migrating Cannon from a 32bit MIPS architecture to a 64bit architecture will allow the heap to grow by many orders of magnitude, effectively providing unlimited memory to the runtime. Multithreaded and 64bit Cannon are currently undergoing audit and should be ready to go live on testnet in the next couple months
In addition, the Base engineering team is working alongside other OP Stack Core Contributors to develop the upcoming Holocene OP Stack hardfork. This upgrade adds partial span batch validation, which reduces resource constraint by removing the need for fault proof programs to execute all L2 blocks in the batch
Now that we’ve developed a foundational set of fault proof tests, tooling, and reference data, we plan to benchmark all future changes to the OP Stack, ensuring that we can continue to safely scale Base and the broader Superchain, and bring the world onchain.
Appendix
In the interest of transparency, we’ve included the raw data from each of our tests below. All tests were performed using Cannon and op-program binaries built from the v1.9.1 op-stack release using go 1.21.
ETH Transfer
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,623,138,719 | 111.39 MB | 381,827 |
2Mgas | 2,000,000 | 6,928,151,521 | 132.27 MB | 492,737 |
3Mgas | 3,000,000 | 6,767,001,159 | 129.62 MB | 463,468 |
20Mgas | 20,000,000 | 6,785,483,671 | 129.92 MB | 448,499 |
Estimates | ||||
20Mgas | 20,000,000 | 16,734,192,426 | 288.43 MB | 1,180,780 |
100Mgas | 100,000,000 | 8,745,359,260 | 161.04 MB | 490,610 |
Observed Error | ||||
20Mgas | 20,000,000 | -146.62% | -122.01% | -163.27% |
ERC20 Transfer
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 6,204,271,686 | 131.99 MB | 452,159 |
2Mgas | 2,000,000 | 6,310,449,198 | 131.40 MB | 413,534 |
3Mgas | 3,000,000 | 6,589,280,079 | 133.50 MB | 482,149 |
20Mgas | 20,000,000 | 11,317,368,094 | 170.07 MB | 791,432 |
Estimates | ||||
20Mgas | 20,000,000 | 9,833,075,858 | 145.90 MB | 719,191 |
100Mgas | 100,000,000 | 33,251,618,820 | 336.95 MB | 2,309,041 |
Observed Error | ||||
20Mgas | 20,000,000 | 13.12% | 14.21% | 9.13% |
Storage Writes
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,765,573,364 | 127.43 MB | 523,396 |
2Mgas | 2,000,000 | 5,779,242,866 | 127.70 MB | 564,251 |
3Mgas | 3,000,000 | 5,794,625,704 | 128.22 MB | 554,143 |
20Mgas | 20,000,000 | 6,258,738,250 | 138.29 MB | 669,946 |
Estimates | ||||
20Mgas | 20,000,000 | 6,041,285,038 | 134.86 MB | 823,986 |
100Mgas | 100,000,000 | 8,378,069,765 | 184.85 MB | 1,221,734 |
Observed Error | ||||
20Mgas | 20,000,000 | 3.47% | 2.48% | -22.99% |
Storage Reads
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,933,015,834 | 127.08 MB | 513,891 |
2Mgas | 2,000,000 | 6,290,868,310 | 129.71 MB | 540,739 |
3Mgas | 3,000,000 | 6,300,316,628 | 127.04 MB | 559,180 |
20Mgas | 20,000,000 | 9,431,852,148 | 126.98 MB | 805,725 |
Estimates | ||||
20Mgas | 20,000,000 | 9,480,440,737 | 127.57 MB | 945,538 |
100Mgas | 100,000,000 | 23,909,994,957 | 122.71 MB | 2,001,824 |
Observed Error | ||||
20Mgas | 20,000,000 | -0.52% | -0.47% | -17.35% |
Deploys of Tiny Contracts
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,777,467,988 | 127.69 MB | 524,860 |
2Mgas | 2,000,000 | 5,973,143,239 | 130.88 MB | 555,883 |
3Mgas | 3,000,000 | 5,998,996,944 | 131.45 MB | 536,078 |
20Mgas | 20,000,000 | 6,164,316,368 | 137.03 MB | 564,437 |
Estimates | ||||
20Mgas | 20,000,000 | 7,910,296,661 | 163.85 MB | 639,902 |
100Mgas | 100,000,000 | 7,339,598,094 | 169.39 MB | 680,956 |
Observed Error | ||||
20Mgas | 20,000,000 | -28.32% | -19.57% | -13.37% |
SHA256 with small inputs
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,983,226,628 | 128.70 MB | 473,537 |
2Mgas | 2,000,000 | 6,201,949,185 | 130.35 MB | 498,090 |
3Mgas | 3,000,000 | 6,232,516,126 | 128.73 MB | 485,517 |
20Mgas | 20,000,000 | 9,330,323,559 | 153.91 MB | 683,860 |
Estimates | ||||
20Mgas | 20,000,000 | 8,382,836,128 | 129.52 MB | 593,535 |
100Mgas | 100,000,000 | 23,472,781,822 | 262.44 MB | 1,560,676 |
Observed Error | ||||
20Mgas | 20,000,000 | 10.15% | 15.85% | 13.21% |
SHA256 with large inputs
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 6,031,109,395 | 130.37 MB | 476,236 |
2Mgas | 2,000,000 | 6,243,997,347 | 133.59 MB | 458,634 |
3Mgas | 3,000,000 | 6,432,598,100 | 135.40 MB | 521,362 |
20Mgas | 20,000,000 | 8,476,207,979 | 162.85 MB | 598,232 |
Estimates | ||||
20Mgas | 20,000,000 | 9,849,299,959 | 178.39 MB | 891,545 |
100Mgas | 100,000,000 | 18,491,349,238 | 295.63 MB | 1,112,098 |
Observed Error | ||||
20Mgas | 20,000,000 | -16.20% | -9.54% | -49.03% |
Deploys of Tiny Contracts
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,933,015,834 | 127.08 MB | 513,891 |
2Mgas | 2,000,000 | 6,290,868,310 | 129.71 MB | 540,739 |
3Mgas | 3,000,000 | 6,300,316,628 | 127.04 MB | 559,180 |
20Mgas | 20,000,000 | 9,431,852,148 | 126.98 MB | 805,725 |
Estimates | ||||
20Mgas | 20,000,000 | 9,480,440,737 | 127.57 MB | 945,538 |
100Mgas | 100,000,000 | 23,909,994,957 | 122.71 MB | 2,001,824 |
Observed Error | ||||
20Mgas | 20,000,000 | -0.52% | -0.47% | -17.35% |
RIPEMD160 with small inputs
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,875,453,802 | 128.06 MB | 441,286 |
2Mgas | 2,000,000 | 5,816,233,429 | 126.04 MB | 450,449 |
3Mgas | 3,000,000 | 6,104,706,376 | 129.61 MB | 459,738 |
20Mgas | 20,000,000 | 7,940,103,065 | 144.32 MB | 577,400 |
Estimates | ||||
20Mgas | 20,000,000 | 7,995,404,368 | 141.91 MB | 616,559 |
100Mgas | 100,000,000 | 16,866,767,431 | 217.19 MB | 1,143,100 |
Observed Error | ||||
20Mgas | 20,000,000 | -0.70% | 1.67% | -6.78% |
RIPEMD160 with large inputs
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 6,118,592,793 | 132.25 MB | 444,495 |
2Mgas | 2,000,000 | 6,291,711,082 | 133.95 MB | 480,941 |
3Mgas | 3,000,000 | 6,444,682,546 | 136.32 MB | 486,372 |
20Mgas | 20,000,000 | 8,167,690,035 | 156.55 MB | 567,438 |
Estimates | ||||
20Mgas | 20,000,000 | 9,219,803,251 | 170.82 MB | 847,496 |
100Mgas | 100,000,000 | 16,579,834,836 | 256.57 MB | 1,009,693 |
Observed Error | ||||
20Mgas | 20,000,000 | -12.88% | -9.12% | -49.35% |
Blake2f
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 6,898,305,181 | 128.29 MB | 511,359 |
2Mgas | 2,000,000 | 8,407,611,074 | 131.24 MB | 625,084 |
3Mgas | 3,000,000 | 9,774,154,416 | 131.31 MB | 721,918 |
20Mgas | 20,000,000 | 33,206,433,070 | 129.59 MB | 2,071,668 |
Estimates | ||||
20Mgas | 20,000,000 | 34,242,666,672 | 157.41 MB | 2,514,485 |
100Mgas | 100,000,000 | 143,678,859,859 | 127.70 MB | 8,544,731 |
Observed Error | ||||
20Mgas | 20,000,000 | -3.12% | -21.47% | -21.37% |
ECAdd
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 7,018,067,400 | 143.96 MB | 549,523 |
2Mgas | 2,000,000 | 7,614,519,602 | 150.25 MB | 584,388 |
3Mgas | 3,000,000 | 8,483,630,982 | 162.28 MB | 643,260 |
20Mgas | 20,000,000 | 15,553,134,512 | 250.38 MB | 1,241,447 |
Estimates | ||||
20Mgas | 20,000,000 | 20,895,478,233 | 317.06 MB | 1,436,023 |
100Mgas | 100,000,000 | 50,658,462,679 | 689.75 MB | 4,134,394 |
Observed Error | ||||
20Mgas | 20,000,000 | -34.35% | -26.63% | -15.67% |
ECMul
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,895,614,790 | 129.00 MB | 498,306 |
2Mgas | 2,000,000 | 6,205,261,740 | 133.21 MB | 501,669 |
3Mgas | 3,000,000 | 6,342,304,197 | 135.02 MB | 551,448 |
20Mgas | 20,000,000 | 8,229,871,844 | 156.94 MB | 971,344 |
Estimates | ||||
20Mgas | 20,000,000 | 10,167,931,572 | 186.60 MB | 995,419 |
100Mgas | 100,000,000 | 17,566,030,097 | 267.25 MB | 2,991,045 |
Observed Error | ||||
20Mgas | 20,000,000 | -23.55% | -18.89% | -2.48% |
ECPairing (accelerated by op-program)
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,776,645,362 | 127.56 MB | 516,213 |
2Mgas | 2,000,000 | 5,963,589,034 | 130.47 MB | 527,470 |
3Mgas | 3,000,000 | 5,828,919,975 | 128.34 MB | 511,751 |
20Mgas | 20,000,000 | 6,271,050,229 | 135.16 MB | 530,233 |
Estimates | ||||
20Mgas | 20,000,000 | 6,326,856,307 | 135.87 MB | 478,320 |
100Mgas | 100,000,000 | 8,116,374,114 | 163.48 MB | 580,276 |
Observed Error | ||||
20Mgas | 20,000,000 | -0.89% | -0.53% | 9.79% |
P256Verify
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 17,049,500,014 | 131.29 MB | 1,277,339 |
2Mgas | 2,000,000 | 27,803,520,661 | 129.99 MB | 1,878,936 |
3Mgas | 3,000,000 | 38,780,421,364 | 133.84 MB | 2,585,735 |
20Mgas | 20,000,000 | 210,254,748,805 | 155.70 MB | 13,078,063 |
Estimates | ||||
20Mgas | 20,000,000 | 223,456,106,163 | 154.60 MB | 13,689,567 |
100Mgas | 120,000,000 | 1,224,139,467,745 | 288.95 MB | 75,132,093 |
Observed Error | ||||
20Mgas | 20,000,000 | -6.28% | 0.71% | -4.68% |
Identity
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,948,955,650 | 132.48 MB | 440,868 |
2Mgas | 2,000,000 | 6,071,516,330 | 137.65 MB | 433,790 |
3Mgas | 3,000,000 | 6,170,595,841 | 140.73 MB | 441,160 |
20Mgas | 20,000,000 | 7,199,877,925 | 171.29 MB | 504,847 |
Estimates | ||||
20Mgas | 20,000,000 | 8,058,450,993 | 211.24 MB | 441,234 |
100Mgas | 100,000,000 | 12,286,011,877 | 325.56 MB | 796,554 |
Observed Error | ||||
20Mgas | 20,000,000 | -11.92% | -23.32% | 12.60% |
Modexp
Label | Gas Usage | Instructions | Memory Usage | Runtime (ms) |
Samples | ||||
1Mgas | 1,000,000 | 5,977,197,865 | 131.11 MB | 413,460 |
2Mgas | 2,000,000 | 6,022,014,219 | 132.55 MB | 420,963 |
3Mgas | 3,000,000 | 5,892,528,187 | 130.86 MB | 411,115 |
20Mgas | 20,000,000 | 6,494,391,967 | 150.27 MB | 496,198 |
Estimates | ||||
20Mgas | 20,000,000 | 5,201,886,322 | 129.22 MB | 394,074 |
100Mgas | 100,000,000 | 8,797,267,453 | 232.79 MB | 851,951 |
Observed Error | ||||
20Mgas | 20,000,000 | 19.90% | 14.01% | 20.58% |