The Future of Scaling Ethereum: Scroll ZkEvm x Noir

The Future of Scaling Ethereum: Scroll ZkEvm x Noir

Scroll is zero-knowledge (ZK) Ethereum Virtual Machine (EVM) layer 2 that launched in October 2023. Its aim is to scale Ethereum by processing transactions off-chain and generating proofs of their validity. These proofs are then sent to the Ethereum mainnet for verification and inclusion.

It is an an open source, bytecode-compatible which allows developers to deploy pre-existing smart contracts from Ethereum directly on Scroll without modifying the underlying code which helps to achieve an identical development and use experience to Ethereum’s virtual machine and tools with aim of use zk-rollups to tackle Ethereum’s scalability issues.

How Scroll Works

Scroll chain is composed of three layers as mentioned in the above diagram :

Settlement Layer : It enables users and dapps to transfer assets and messages between Ethereum and Scroll, checks validity proofs, and provides data availability and sorting for the core Scroll chain. We implement the rollup contract and bridge onto Ethereum, which we'll use as the Settlement Layer.

Sequencing layer : It is made up of an execution node that carries out the transactions submitted to the L1 bridge contract and the Scroll sequencer, producing L2 blocks, and a rollup node that aggregates transactions, posts block and transaction data to Ethereum for data availability, and sends validity proofs to Ethereum for finality.

Proving Layer : It is made up of a pool of provers that produce the zkEVM validity proofs needed to verify the accuracy of L2 transactions, and an admin who distributes proving tasks to provers and sends the proofs to the Rollup Node for Ethereum finalisation.

Noir

Noir, a domain-specific language designed for SNARK proving systems, distinguishes out due to its simplicity, versatility, and powerful features. It first compiles to an adaptive intermediate language called ACIR.

It simplifies the construction of Solidity contracts that communicate with SNARK systems. Use the nargo codegen-verifier tool to efficiently generate verifier contracts. Noir presently includes a command for creating a Solidity contract that verifies your Noir programme.

Benefits of Using Noir

  1. Multiple Capabilities : Noir stands out for its simplicity, versatility, and powerful features, providing developers with a flexible toolkit for cryptographic programming.

  2. Modular Design : Noir's revolutionary design separates the programming language from the backend, following the modular idea of LLVM. This separation facilitates connection with many backend systems, giving developers more flexibility in selecting and customising proving systems.

  3. Efficient Solidity Contract Creation : Noir makes it easier for Solidity developers to create contracts that interface with SNARK systems. The nargo codegen-verifier tool efficiently generates verifier contracts, and future releases seek to modularize this process for even greater simplicity of usage.

  4. Flexibility : Noir's compilation to a proof-agnostic intermediate language provides unprecedented flexibility for protocol engineers. This enables the integration of various proving systems, allowing engineers to customise the proving system to their individual needs and technology stack.

  5. Custom System Integration : Noir's ability to integrate bespoke proving system backends and smart contract interfaces is useful for blockchain developers. This guarantees easy integration with blockchain architectures, overcomes environmental limits, and expands project innovation potential.

Zk Benfits in Contracts

The field of smart contracts has undergone exciting developments thanks to zero-knowledge proofs (ZKPs), which provide a powerful combination of security, scalability, and privacy. The following are some main advantages of smart contracts that use ZK proofs:

  1. Enhanced transparency : Transactions that are confidential , ZKPs let users demonstrate that they meet specific requirements (such age or income) without disclosing the actual underlying data. This maintains regulatory compliance while safeguarding private data.

  2. Enhanced Scalability : Diminished on-chain data , ZKPs significantly decrease the quantity of data maintained on the blockchain by proving operations off-chain and providing only a brief proof on-chain. Lower fees and quicker transaction processing are the results of this.

  3. Enhanced Safety : ZKPs can be used to demonstrate that a user possesses particular assets or meets eligibility conditions before to interacting with a smart contract, hence preventing fraud. This reduces the possibility of fraud and illegal access.

Zk Proof System in Scroll

Scroll utilizes a Hierarchical Zero-Knowledge Proof System. The system contains 2 separate layers adopting different zero-knowledge proof systems.

  1. The first layer generates proofs for different dApp to ensure fast

    proving with short proving keys, and high scalability by using low complexity proofs.
    The first requirement enables people to efficiently construct proofs even on their own. This enables privacy-preserving smart contracts in the future with significantly less proving work on the user's part. Users can also outsource proof generation to miners, who will generate proofs effectively without needing to keep big proving keys for multiple DApps. The second and third conditions allow all smart contract logic to be maintained without a trusted setup, regardless of how complex the calculation is. The use of a zero-knowledge proof system in the first layer may sacrifice verification time and proof size in order to achieve these requirements.

  2. The second layer then generates proofs from the proofs made in the first layer. Essentially, this layer acts as a wrapping layer. This results in shorter

    proofs and a more EVM-compatible proving system.

    These two requirements improve on-chain verification efficiency. The wrapping layer serves as a universal on-chain verifier. With this wrapping, the on-chain verification smart contract might become common for a variety of DApps. Miners will create proofs for this layer using a universal proving key. The use of a zero-knowledge proof system may sacrifice proving time in order to meet these requirements.

ZK Circuit Solidity with Noir

Ethereum is one of the well-known blockchain systems that may be used to write smart contracts using Solidity, a high-level programming language.

Building a Quadratic Voting using Zk Proofs

It's time to put the concepts into practice now that you have a solid understanding of them.

Head over to the git repository to get the starter file for your ZK-proof quadratic voting contract which is divided into 2 sub components where one can cast vote and other can count votes.

Note: You can either choose to run the application locally or on GitHub codespace.

Now you have a running workspace with your starter code:

  1. The noir/cast-ballot/src/main.nr file is where you have your major the nargo code that will help one user to cast and generates your zk-proofing smart contract for verifying your voting smart contract and vote the specific user .

  2. First, run the command yarn to initialize a basic node environment.

  3. Next, run the command curl -Lhttps://raw.githubusercontent.com/noir-lang/noirup/main/install| bash in your terminal.
    to install nargo.

  4. Reopen a new terminal and run the command noirup, then noirup -v 1.0.0, to install the nargo version 1.0.0, as it is the latest confirmed version confirmed to be compatible with the code in this tutorial.

    Note: Run the code noir/cast-ballot/src/main.nr to confirm your running nargo on the right version ~1.0.0

  5.    use dep::std;
       global CANDIDATE_COUNT = 10;
    
       fn main(
           token_budget: pub u32, // budget of max votes to enforce
           votes: [u32;CANDIDATE_COUNT], // ballot of user representing it's votes for each candidate
           secret: Field // for shielding the commitment from brute-force attacks
       ) -> pub Field{
           check_within_budget(token_budget, votes); 
           calculate_ballot_commitment(votes, secret)
       }
    
       //The proof that we will generate ensures the user respected the token_budget
       //because we have an explicit constrain here
       fn check_within_budget(token_budget: u32, votes: [u32;CANDIDATE_COUNT]) {
           let mut sum: u32 = 0;
           for i in 0..CANDIDATE_COUNT {
               sum = sum + votes[i] * votes[i];
           }
           constrain sum <= token_budget;
       }
    
       // Hashes the content of a ballot using pedersen and returns that value
       fn calculate_ballot_commitment(votes: [u32; CANDIDATE_COUNT], secret: Field) -> Field{
           let mut input = [0; CANDIDATE_COUNT + 1];
           input[0] = secret;
           for i in 0..CANDIDATE_COUNT {
               input[i+1] = votes[i] as Field;
           }
           let commitment = std::hash::pedersen(input)[0];
           commitment
    
  6. And if u want to migrate to counting of votes , then the path location is noir/count-votes/src/main.nr and repeat the above steps .

  7.    use dep::std;
       global CANDIDATE_COUNT = 10;
       global VOTER_COUNT = 2;
    
       fn main(
           commitments: [Field; VOTER_COUNT], // public commitments for each voter
           secrets: [Field; VOTER_COUNT], // Each user's ballot secret
           all_votes: [u32; VOTER_COUNT * CANDIDATE_COUNT] //Flattened array of user's votes. Votes are adjacent for each user
       ) -> pub [u32; CANDIDATE_COUNT] {
           check_commitments(commitments, secrets, all_votes);
           let result = sum_votes(all_votes);
           result
       }
    
       // Iterates over the flattened array of votes and returns an array
       // with the sum of votes for each candidate
       fn sum_votes(all_votes: [u32; VOTER_COUNT * CANDIDATE_COUNT]) -> [u32; CANDIDATE_COUNT] {
           let mut result = [0; CANDIDATE_COUNT];
           for i in 0..VOTER_COUNT {
               for j in 0..CANDIDATE_COUNT {
                   result[j] = result[j] + all_votes[CANDIDATE_COUNT * i + j];
               }
           };
           result
       }
    
       // Recalculate commitment for each user vote and compare it to the provided commitment
       fn check_commitments(
           commitments: [Field; VOTER_COUNT], 
           secrets: [Field; VOTER_COUNT], 
           all_votes: [u32; VOTER_COUNT * CANDIDATE_COUNT]
       ) {     
           let mut input = [0; CANDIDATE_COUNT + 1];
           for i in 0..VOTER_COUNT {
               //get secret for voter i
               input[0] = secrets[i]; 
               for j in 0..CANDIDATE_COUNT {
                   input[j+1] = all_votes[CANDIDATE_COUNT * i + j] as Field;
               }
               // calculate commitment for voter i
               let commitment = std::hash::pedersen(input)[0];
               // verify commitment against input
               constrain commitment == commitments[i];
           };
       }
    

Specifications of Multiple Function

  1. Casting of votes

    Defines constants and functions:

    • CANDIDATE_COUNT: Sets the number of candidates in the election to 10.

    • main: Takes three inputs:

      • token_budget: The maximum number of votes allowed per user.

      • votes: An array representing the user's votes for each candidate.

      • secret: A secret value used for privacy protection.

    • check_within_budget: Verifies that the total number of votes cast doesn't exceed the token_budget.

    • calculate_ballot_commitment: Creates a commitment to the user's vote in a way that hides the specific votes but allows verification that the budget constraint was met.

Main function:

  • Calls check_within_budget to ensure the vote adheres to the budget limit.

  • Calls calculate_ballot_commitment to create a commitment to the vote.

  • Returns the created commitment.

check_within_budget function:

  • Calculates the sum of squared votes (not the actual number of votes).

  • Uses a mathematical statement (constrain) to verify that the sum is less than or equal to the token_budget.

calculate_ballot_commitment function:

  • Combines the secret value with the votes (converted to a specific data type) into a single array.

  • Uses a cryptographic hash function called pedersen to create a commitment from the combined array.

  • Returns the generated commitment.

  1. Counting of Votes

Imports and Globals:

  • dep::std: Imports standard library functions (likely using a custom library named dep).

  • CANDIDATE_COUNT: Defines the number of candidates in the election.

  • VOTER_COUNT: Defines the number of voters participating.

Main Function:

  • Takes three arguments:

    • commitments: An array of public commitments for each voter.

    • secrets: An array of secret ballots for each voter.

    • all_votes: A flattened array containing all votes for each candidate, organized by voter.

  • Calls check_commitments to verify the integrity of the votes.

  • Calls sum_votes to count the votes for each candidate.

  • Returns the final vote count for each candidate.

Sum_Votes Function:

  • Takes the flattened array of votes as input.

  • Initializes an array result with zeros for each candidate.

  • Iterates through each voter and candidate:

    • Adds the corresponding vote from all_votes to the result for that candidate.
  • Returns the array result containing the summed votes for each candidate.

Check_Commitments Function:

  • Takes the commitments, secrets, and all votes as input.

  • Initializes an array input to store the secret and each vote for a specific voter.

  • Iterates through each voter:

    • Sets input[0] to the voter's secret from the secrets array.

    • Sets input[j+1] to the corresponding vote for each candidate (j) from the all_votes array, converting it to a Field type.

    • Calculates a commitment for the voter using a pedersen hashing function on the input array.

    • Constrains the calculated commitment to match the provided commitment for that voter in the commitments array.

This ensures that the votes used for counting match the commitments previously published by each voter, upholding privacy while verifying the integrity of their votes.

Deploying on Scroll

To deploy your contract on the scroll sapolia test network

  1. Head over to the link to get scroll sapolia faucets (https://sepoliafaucet.com/)

  2. Run forge create --rpc-url forge create --rpc-urlhttps://alpha-rpc.scroll.io/l2src/zkVoting.sol:Voting --private-key <enter your PRIVATE_KEY>, to install your private key and scroll's rpc URL in your environment.

  3. Finally, run the command forge script scripts/Voting.s.sol:DeploymentScript --rpc-urlhttps://sepolia-rpc.scroll.io--broadcast --verify -vvvv to deploy to scroll and you will get a response like the image below.

Conclusion

ZK technology presents an achievable approach for resolving blockchain scaling issues, and Scroll is essential to the integration of this technology into Ethereum to ensure that ZKPs are widely adopted, however, and to solve the technological challenges, more research and development are required.

Other ZK-rollups methods are under development, each with advantages and disadvantages of its own. Ethereum's long-term scalability is likely to depend on a variety of strategies, such as ZK-rollups, more layer-2 solutions, and perhaps updates to the Ethereum protocol itself.