Grains in Cairo: Complete Solution & Deep Dive Guide

a wall with egyptian writing and birds on it

The Ultimate Guide to Cairo's Grains Problem: Mastering Exponents & Large Numbers

The Cairo Grains problem is a classic computational challenge that elegantly demonstrates the power of exponential growth. This guide breaks down how to calculate the number of wheat grains on any chessboard square and the total sum, using Cairo's features for numerical precision, assertions, and performance optimization.


Have you ever encountered a problem that seems deceptively simple on the surface, only to realize it scales to an astronomical size? This is a common hurdle in programming, especially in resource-constrained environments like blockchains. Mismanaging large numbers or inefficient calculations can lead to bugs, failed transactions, and wasted gas fees.

This is where the ancient "wheat and chessboard problem" becomes a powerful teaching tool. It's not just a brain teaser; it's a perfect practical exercise from the exclusive kodikra.com curriculum for understanding core concepts in Cairo. In this deep dive, we will dissect this problem, explore multiple solutions, and reveal why one approach is vastly superior for real-world smart contract development on StarkNet. You'll learn to handle large integers, validate inputs, and write gas-efficient code—transforming a simple story into a profound lesson in computational thinking.


What is the Grains on the Chessboard Problem?

The problem originates from a famous legend about the inventor of chess. As a reward, the inventor asked the king for a seemingly humble payment: one grain of wheat on the first square of a chessboard, two on the second, four on the third, and so on, doubling the amount for each of the 64 squares.

The king, amused by the modest request, agreed without a second thought. He soon discovered his monumental error. The doubling effect, an example of a geometric progression, results in an unimaginably large number of grains, far exceeding all the wheat in the world.

In our Cairo programming challenge, this story is broken down into two specific tasks:

  1. Calculate Grains on a Single Square: Write a function that takes a square number (from 1 to 64) as input and returns the exact number of grains on that specific square.
  2. Calculate the Total Grains: Write a function that calculates the sum of all grains on all 64 squares of the chessboard.

Solving this requires a solid grasp of mathematical exponentiation and the ability to work with large integer types that can accommodate the massive numbers involved.


Why is This a Foundational Problem for Learning Cairo?

While simple, the Grains problem is an exceptional module for aspiring Cairo and StarkNet developers. It forces you to engage with concepts that are non-negotiable for writing secure and efficient smart contracts.

  • Integer Types and Overflow: The number of grains grows incredibly fast. The final square alone has a number so large it requires a 64-bit unsigned integer (u64). The total number perfectly fits within a u64, but understanding these limits is crucial. This exercise makes you think about data types like u8, u64, u128, and u256 and when to use them to prevent costly overflow errors.
  • Input Validation with assert!: A chessboard only has 64 squares. What should your function do if someone provides an input of 0 or 65? In smart contracts, invalid inputs can be exploited. The Grains problem is a perfect scenario to learn and apply the assert! macro to enforce preconditions, a fundamental security practice.
  • Computational Efficiency and Gas Costs: As we will see, there are multiple ways to calculate the total number of grains. A naive, iterative approach works but performs 64 calculations. A more intelligent, mathematical approach finds the answer in a single operation. In the world of StarkNet, fewer operations directly translate to lower gas fees for users, making this a vital lesson in optimization.
  • Core Language Syntax: It provides a practical context for learning fundamental Cairo syntax, including function definitions, parameters, return types, loops, and using methods from the core library.

How to Calculate Grains on a Single Square in Cairo

The number of grains on any given square 'n' follows a clear mathematical pattern. Square 1 has 20 (1) grains, Square 2 has 21 (2) grains, Square 3 has 22 (4) grains, and so on. The formula is therefore 2(n-1), where 'n' is the square number from 1 to 64.

Let's translate this logic into a Cairo function.

The Code: square() function

use core::num::traits::Pow;

pub fn square(s: u8) -> u64 {
    assert!(1 <= s && s <= 64, "square must be between 1 and 64");
    2_u64.pow(s.into() - 1)
}

Line-by-Line Code Walkthrough

This function, though short, is packed with important Cairo concepts. Let's break it down piece by piece.

use core::num::traits::Pow;

  • Purpose: This line imports the Pow trait. In Cairo, traits are similar to interfaces in other languages; they define a set of methods that a type can implement. The Pow trait provides the .pow() method for performing exponentiation.
  • Details: We need this to calculate 2 to the power of something. By bringing Pow into scope, we enable integer types like u64 to use this functionality.

pub fn square(s: u8) -> u64 { ... }

  • pub fn: This declares a public function named square, meaning it can be called from other modules or contracts.
  • s: u8: This defines the function's single parameter. The name is s (for square), and its type is u8. A u8 is an 8-bit unsigned integer, which can hold values from 0 to 255. This is a perfect choice for the square number, as we only need to represent values from 1 to 64. Using the smallest necessary integer type is good practice for efficiency.
  • -> u64: This specifies the function's return type. The function will return a 64-bit unsigned integer (u64). We need a large type because the number of grains on the 64th square is 263, a massive number that would overflow smaller types like u8, u16, or u32. A u64 is the smallest standard type that can hold it.

assert!(1 <= s && s <= 64, "square must be between 1 and 64");

  • Purpose: This is the crucial input validation step. The assert! macro checks if a given condition is true.
  • Logic: It verifies that the input s is greater than or equal to 1 AND less than or equal to 64. If this condition is false, the program will panic and terminate execution, printing the error message provided as the second argument.
  • Why it's critical: In a smart contract, this prevents the function from being called with invalid data that could lead to unexpected behavior, wasted gas, or security vulnerabilities. It enforces the problem's constraints at the function's entry point.

2_u64.pow(s.into() - 1)

  • The Core Logic: This line implements our formula, 2(n-1).
  • 2_u64: This is the number literal 2, but with a type suffix _u64. This explicitly tells the Cairo compiler to treat this 2 as a u64 value. This is necessary because the .pow() method is being called on it.
  • .pow(...): This is the exponentiation method provided by the Pow trait we imported. It raises the base (2_u64) to the power of the exponent provided in the parentheses.
  • s.into() - 1: This is the exponent.
    • s - 1 is incorrect on its own because `s` is a `u8`. The `.pow()` method for `u64` expects an exponent of type `u32`.
    • s.into() is the magic here. The .into() method is a convenient way to perform type conversion. The compiler infers that we need a u32 for the exponent and converts `s` (a `u8`) into a `u32`.
    • Finally, we subtract 1 to match our formula 2(n-1).

An Alternative using Bit Shifting

For powers of two, a more idiomatic and often more performant approach in systems programming is to use the left bit shift operator (<<). The expression 1 << x is equivalent to 2x.

Our formula is 2(s-1). We can rewrite this using a bit shift.

pub fn square_optimized(s: u8) -> u64 {
    assert!(1 <= s && s <= 64, "square must be between 1 and 64");
    // 1_u64 shifted left by (s - 1) positions is equivalent to 2^(s-1)
    1_u64 << (s - 1)
}

This version avoids the external trait import and can be compiled down to a single, highly efficient CPU instruction, making it the preferred solution for performance-critical code.


How to Calculate the Total Grains on the Chessboard

Now for the second task: summing the grains from all 64 squares. There are two main ways to approach this: the straightforward loop and the elegant mathematical shortcut.

Method 1: The Iterative (Looping) Approach

The most intuitive way to solve this is to loop through each square from 1 to 64, calculate the grains on that square using our square() function, and add it to a running total.

// Assumes the `square` function from above is available
pub fn total() -> u64 {
    let mut result: u64 = 0;
    for s in 1..65_u8 {
        result += square(s);
    }
    result
}

Code Walkthrough

  1. let mut result: u64 = 0;: We declare a mutable variable named result, explicitly typed as a u64, and initialize it to 0. This variable will accumulate our total.
  2. for s in 1..65_u8 { ... }: This is a for loop.
    • The range 1..65_u8 creates an iterator that goes from 1 up to (but not including) 65. This gives us the numbers 1, 2, 3, ..., 64.
    • The _u8 suffix on 65 tells the compiler the range is over u8 values, matching the type of s.
    • In each iteration, the current square number is assigned to the variable s.
  3. result += square(s);: Inside the loop, we call our previously defined square(s) function to get the number of grains for the current square. We then add this value to our result using the += compound assignment operator.
  4. result: In Cairo (like Rust), if the last expression in a function is not followed by a semicolon, it is implicitly returned. This line returns the final calculated total.

Here is a visual representation of the iterative logic:

    ● Start
    │
    ▼
  ┌──────────────────┐
  │ Initialize total = 0 │
  │ Initialize square = 1│
  └─────────┬────────┘
            │
            ▼
    ◆ square <= 64?
   ╱           ╲
  Yes           No
  │              │
  ▼              ▼
┌──────────────────┐   ┌───────────────┐
│ grains = 2^(s-1) │   │ Return total  │
└─────────┬────────┘   └───────────────┘
          │                    ▲
          ▼                    │
┌──────────────────┐           ● End
│ total += grains  │
└─────────┬────────┘
          │
          ▼
┌──────────────────┐
│ square++         │
└─────────┬────────┘
          │
          └───────────⟶ (Back to Condition)

While this approach is correct and easy to understand, it's computationally expensive. It requires 64 calls to the square function and 64 addition operations. For a blockchain, this is far from ideal.

Method 2: The Optimized Mathematical Shortcut

A much better solution leverages a property of geometric series. The sum of the series 1 + 2 + 4 + ... + 2(n-1) is equal to 2n - 1.

For our chessboard with n=64 squares, the total number of grains is 264 - 1.

This number has a very special property in computer science. A 64-bit unsigned integer (u64) can store values from 0 to 264 - 1. Therefore, the total number of grains is simply the maximum possible value a u64 can hold!

This simplifies our function dramatically.

pub fn total_optimized() -> u64 {
    // The sum of grains on all 64 squares is 2^64 - 1,
    // which is the maximum value for a u64 integer.
    u64::MAX
}

This optimized version is breathtakingly simple and efficient. It involves no loops and no complex calculations. It's a single, constant-time operation that directly returns the known maximum value of a u64. In a smart contract context, this is the professionally preferred solution, as its gas cost is minimal and constant, regardless of the number of squares.

The logic flow for this superior approach is much simpler:

    ● Start
    │
    ▼
  ┌──────────────────────────────┐
  │ Identify Problem: Sum of 2^k │
  │ for k = 0 to 63              │
  └──────────────┬───────────────┘
                 │
                 ▼
  ┌──────────────────────────────┐
  │ Apply Formula: 2^64 - 1      │
  └──────────────┬───────────────┘
                 │
                 ▼
  ┌──────────────────────────────┐
  │ Recognize as u64::MAX        │
  └──────────────┬───────────────┘
                 │
                 ▼
           ● End (Return Constant)

Comparing the Approaches: Pros & Cons

Let's formally compare the two methods for calculating the total.

Metric Iterative (Looping) Approach Optimized (Mathematical) Approach
Performance / Gas Cost Low (Poor). Proportional to the number of squares (O(n)). Involves 64 function calls, 64 exponentiations, and 64 additions. Extremely High (Excellent). Constant time (O(1)). A single operation to return a constant value. Minimal gas cost.
Readability High. The logic is very explicit and easy for beginners to follow. It directly models the problem statement. Medium. Requires knowledge of the underlying mathematical shortcut. The code itself is simpler, but the "why" is less obvious without comments.
Flexibility More flexible. Could be easily adapted to sum grains over a partial board (e.g., squares 1 to 32) by changing the loop range. Less flexible. Hardcoded to solve for exactly 64 squares. Adapting it would require recalculating the formula.
Production Readiness Not recommended for production smart contracts due to high and variable gas costs. Highly recommended. This is the ideal implementation for a production environment.

Where These Concepts Apply in Real-World Cairo Development

Mastering the lessons from this kodikra module directly translates to building robust applications on StarkNet.

  • DeFi Protocols: Financial calculations in DeFi often involve large numbers for token amounts (many tokens use 18 decimal places). Understanding integer types like u128 and u256 is essential to accurately represent balances and prevent overflow when calculating interest or swap amounts.
  • Smart Contract Security: The use of assert! for input validation is a cornerstone of smart contract security. You must always validate inputs for function parameters like token amounts, addresses, and IDs to prevent exploits and ensure the contract operates only within its defined rules.
  • Gas Optimization: The difference between the iterative and optimized total function is a masterclass in gas optimization. StarkNet developers are constantly seeking ways to reduce computational complexity to make their applications cheaper for users. Choosing the right algorithm can reduce gas fees by orders of magnitude.
  • NFTs and Gaming: On-chain games and NFT projects often use numerical traits or IDs. Efficiently calculating properties or validating states using bit shifting and mathematical shortcuts can make the game logic more performant and affordable to run on the blockchain.

Frequently Asked Questions (FAQ)

Why does the square number `s` need to be between 1 and 64?
The problem is defined by the context of a standard 8x8 chessboard, which has exactly 64 squares. The assert!(1 <= s && s <= 64, ...) check enforces this real-world constraint, ensuring the function doesn't produce nonsensical results for invalid inputs like square 0 or 100. This is a fundamental principle of defensive programming.
What exactly does `s.into()` do in the `pow` function call?
s.into() performs a type conversion. The parameter s is a u8, but the pow method for a u64 base requires its exponent to be a u32. .into() is a smart and concise way to convert the u8 value of s into the required u32 type so it can be used in the calculation. The compiler infers the target type from the context.
Why use `u64`? Can it overflow?
A u64 is chosen because it's the smallest standard integer type that can hold the number of grains on the 64th square (263). A u32, for example, would overflow. The total number of grains, 264 - 1, is the absolute maximum value a u64 can hold. If we were to calculate the grains for a hypothetical 65th square (264), it would overflow a u64, highlighting the importance of selecting the correct data type.
Is the iterative `total` function efficient for a smart contract?
Absolutely not. Loops that run a fixed but large number of times are very gas-intensive. Each iteration consumes computational resources (gas). The optimized version, which returns a constant u64::MAX, performs a single operation and has a minimal, constant gas cost. It is vastly more efficient and the only acceptable solution for a production environment.
What is the difference between `u64`, `u128`, and `u256` in Cairo?
These are all unsigned integer types, differing in the number of bits they use for storage, which determines their maximum value. u64 uses 64 bits (max value ~1.8 x 1019). u128 uses 128 bits. u256 uses 256 bits and is commonly used in blockchain development (e.g., Ethereum's standard) to handle cryptographic values and large token balances with high precision.
How does this problem relate to binary numbers?
There's a beautiful connection. A 64-bit integer is a sequence of 64 binary digits (bits). The number of grains on square 'n' (which is 2n-1) is represented in binary as a 1 followed by (n-1) zeros. For example, on square 3, grains = 4, which is 100 in binary. The total sum (264 - 1) is represented in binary as a sequence of 64 ones (1111...1111), which is the maximum value a u64 can store.

Conclusion: From Simple Stories to Powerful Code

The Grains on the Chessboard problem is a perfect illustration of how a simple premise can teach profound software engineering principles. By solving it in Cairo, we've explored far more than just a math puzzle. We've dived into the practical necessities of smart contract development: strict input validation, careful management of integer types to prevent overflows, and the relentless pursuit of computational efficiency to save on gas fees.

The journey from a naive looping solution to an elegant, one-line mathematical formula encapsulates the mindset of a skilled blockchain developer. It's about understanding the deep connection between mathematics, computer science, and the economic realities of a decentralized environment. These foundational skills are precisely what you'll build upon as you progress through more advanced challenges.

Disclaimer: The code in this article is written for Cairo v2.6.3. Future versions of the Cairo language and its core library may introduce changes to syntax or function signatures. Always consult the official documentation for the latest standards.

Ready to apply these concepts to more complex scenarios? Continue your journey by exploring the complete Cairo 2 Learning Path on kodikra.com and deepen your expertise by visiting our comprehensive Cairo programming language guide.


Published by Kodikra — Your trusted Cairo learning resource.