Trade stocks 24/7 on Base.

Learn More

Engineering

Scaling Base: Accelerating Decentralization

Brian Bland

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.\

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,00017,049,500,014131.29 MB1,277,339
2Mgas2,000,00027,803,520,661129.99 MB1,878,936
3Mgas3,000,00038,780,421,364133.84 MB2,585,735
20Mgas20,000,000210,254,748,805155.70 MB13,078,063
Estimates
20Mgas20,000,000223,456,106,163154.60 MB13,689,567
100Mgas100,000,0001,224,139,467,745288.95 MB75,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.\

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0007,018,067,400143.96 MB549,523
2Mgas2,000,0007,614,519,602150.25 MB584,388
3Mgas3,000,0008,483,630,982162.28 MB643,260
20Mgas20,000,00015,553,134,512250.38 MB1,241,447
Estimates
20Mgas20,000,00020,895,478,233317.06 MB1,436,023
100Mgas100,000,00050,658,462,679689.75 MB4,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

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,623,138,719111.39 MB381,827
2Mgas2,000,0006,928,151,521132.27 MB492,737
3Mgas3,000,0006,767,001,159129.62 MB463,468
20Mgas20,000,0006,785,483,671129.92 MB448,499
Estimates
20Mgas20,000,00016,734,192,426288.43 MB1,180,780
100Mgas100,000,0008,745,359,260161.04 MB490,610
Observed Error
20Mgas20,000,000-146.62%-122.01%-163.27%

ERC20 Transfer

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0006,204,271,686131.99 MB452,159
2Mgas2,000,0006,310,449,198131.40 MB413,534
3Mgas3,000,0006,589,280,079133.50 MB482,149
20Mgas20,000,00011,317,368,094170.07 MB791,432
Estimates
20Mgas20,000,0009,833,075,858145.90 MB719,191
100Mgas100,000,00033,251,618,820336.95 MB2,309,041
Observed Error
20Mgas20,000,00013.12%14.21%9.13%

Storage Writes

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,765,573,364127.43 MB523,396
2Mgas2,000,0005,779,242,866127.70 MB564,251
3Mgas3,000,0005,794,625,704128.22 MB554,143
20Mgas20,000,0006,258,738,250138.29 MB669,946
Estimates
20Mgas20,000,0006,041,285,038134.86 MB823,986
100Mgas100,000,0008,378,069,765184.85 MB1,221,734
Observed Error
20Mgas20,000,0003.47%2.48%-22.99%

Storage Reads

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,933,015,834127.08 MB513,891
2Mgas2,000,0006,290,868,310129.71 MB540,739
3Mgas3,000,0006,300,316,628127.04 MB559,180
20Mgas20,000,0009,431,852,148126.98 MB805,725
Estimates
20Mgas20,000,0009,480,440,737127.57 MB945,538
100Mgas100,000,00023,909,994,957122.71 MB2,001,824
Observed Error
20Mgas20,000,000-0.52%-0.47%-17.35%

Deploys of Tiny Contracts

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,777,467,988127.69 MB524,860
2Mgas2,000,0005,973,143,239130.88 MB555,883
3Mgas3,000,0005,998,996,944131.45 MB536,078
20Mgas20,000,0006,164,316,368137.03 MB564,437
Estimates
20Mgas20,000,0007,910,296,661163.85 MB639,902
100Mgas100,000,0007,339,598,094169.39 MB680,956
Observed Error
20Mgas20,000,000-28.32%-19.57%-13.37%

SHA256 with small inputs

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,983,226,628128.70 MB473,537
2Mgas2,000,0006,201,949,185130.35 MB498,090
3Mgas3,000,0006,232,516,126128.73 MB485,517
20Mgas20,000,0009,330,323,559153.91 MB683,860
Estimates
20Mgas20,000,0008,382,836,128129.52 MB593,535
100Mgas100,000,00023,472,781,822262.44 MB1,560,676
Observed Error
20Mgas20,000,00010.15%15.85%13.21%

SHA256 with large inputs

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0006,031,109,395130.37 MB476,236
2Mgas2,000,0006,243,997,347133.59 MB458,634
3Mgas3,000,0006,432,598,100135.40 MB521,362
20Mgas20,000,0008,476,207,979162.85 MB598,232
Estimates
20Mgas20,000,0009,849,299,959178.39 MB891,545
100Mgas100,000,00018,491,349,238295.63 MB1,112,098
Observed Error
20Mgas20,000,000-16.20%-9.54%-49.03%

Deploys of Tiny Contracts

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,933,015,834127.08 MB513,891
2Mgas2,000,0006,290,868,310129.71 MB540,739
3Mgas3,000,0006,300,316,628127.04 MB559,180
20Mgas20,000,0009,431,852,148126.98 MB805,725
Estimates
20Mgas20,000,0009,480,440,737127.57 MB945,538
100Mgas100,000,00023,909,994,957122.71 MB2,001,824
Observed Error
20Mgas20,000,000-0.52%-0.47%-17.35%

RIPEMD160 with small inputs

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,875,453,802128.06 MB441,286
2Mgas2,000,0005,816,233,429126.04 MB450,449
3Mgas3,000,0006,104,706,376129.61 MB459,738
20Mgas20,000,0007,940,103,065144.32 MB577,400
Estimates
20Mgas20,000,0007,995,404,368141.91 MB616,559
100Mgas100,000,00016,866,767,431217.19 MB1,143,100
Observed Error
20Mgas20,000,000-0.70%1.67%-6.78%

RIPEMD160 with large inputs

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0006,118,592,793132.25 MB444,495
2Mgas2,000,0006,291,711,082133.95 MB480,941
3Mgas3,000,0006,444,682,546136.32 MB486,372
20Mgas20,000,0008,167,690,035156.55 MB567,438
Estimates
20Mgas20,000,0009,219,803,251170.82 MB847,496
100Mgas100,000,00016,579,834,836256.57 MB1,009,693
Observed Error
20Mgas20,000,000-12.88%-9.12%-49.35%

Blake2f

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0006,898,305,181128.29 MB511,359
2Mgas2,000,0008,407,611,074131.24 MB625,084
3Mgas3,000,0009,774,154,416131.31 MB721,918
20Mgas20,000,00033,206,433,070129.59 MB2,071,668
Estimates
20Mgas20,000,00034,242,666,672157.41 MB2,514,485
100Mgas100,000,000143,678,859,859127.70 MB8,544,731
Observed Error
20Mgas20,000,000-3.12%-21.47%-21.37%

ECAdd

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0007,018,067,400143.96 MB549,523
2Mgas2,000,0007,614,519,602150.25 MB584,388
3Mgas3,000,0008,483,630,982162.28 MB643,260
20Mgas20,000,00015,553,134,512250.38 MB1,241,447
Estimates
20Mgas20,000,00020,895,478,233317.06 MB1,436,023
100Mgas100,000,00050,658,462,679689.75 MB4,134,394
Observed Error
20Mgas20,000,000-34.35%-26.63%-15.67%

ECMul

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,895,614,790129.00 MB498,306
2Mgas2,000,0006,205,261,740133.21 MB501,669
3Mgas3,000,0006,342,304,197135.02 MB551,448
20Mgas20,000,0008,229,871,844156.94 MB971,344
Estimates
20Mgas20,000,00010,167,931,572186.60 MB995,419
100Mgas100,000,00017,566,030,097267.25 MB2,991,045
Observed Error
20Mgas20,000,000-23.55%-18.89%-2.48%

ECPairing (accelerated by op-program)

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,776,645,362127.56 MB516,213
2Mgas2,000,0005,963,589,034130.47 MB527,470
3Mgas3,000,0005,828,919,975128.34 MB511,751
20Mgas20,000,0006,271,050,229135.16 MB530,233
Estimates
20Mgas20,000,0006,326,856,307135.87 MB478,320
100Mgas100,000,0008,116,374,114163.48 MB580,276
Observed Error
20Mgas20,000,000-0.89%-0.53%9.79%

P256Verify

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,00017,049,500,014131.29 MB1,277,339
2Mgas2,000,00027,803,520,661129.99 MB1,878,936
3Mgas3,000,00038,780,421,364133.84 MB2,585,735
20Mgas20,000,000210,254,748,805155.70 MB13,078,063
Estimates
20Mgas20,000,000223,456,106,163154.60 MB13,689,567
100Mgas120,000,0001,224,139,467,745288.95 MB75,132,093
Observed Error
20Mgas20,000,000-6.28%0.71%-4.68%

Identity

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,948,955,650132.48 MB440,868
2Mgas2,000,0006,071,516,330137.65 MB433,790
3Mgas3,000,0006,170,595,841140.73 MB441,160
20Mgas20,000,0007,199,877,925171.29 MB504,847
Estimates
20Mgas20,000,0008,058,450,993211.24 MB441,234
100Mgas100,000,00012,286,011,877325.56 MB796,554
Observed Error
20Mgas20,000,000-11.92%-23.32%12.60%

Modexp

LabelGas UsageInstructionsMemory UsageRuntime (ms)
Samples
1Mgas1,000,0005,977,197,865131.11 MB413,460
2Mgas2,000,0006,022,014,219132.55 MB420,963
3Mgas3,000,0005,892,528,187130.86 MB411,115
20Mgas20,000,0006,494,391,967150.27 MB496,198
Estimates
20Mgas20,000,0005,201,886,322129.22 MB394,074
100Mgas100,000,0008,797,267,453232.79 MB851,951
Observed Error
20Mgas20,000,00019.90%14.01%20.58%

Start building on Base.

Build on the open economy. Fast, liquid and always on.