Master Election Day in Cairo: Complete Learning Path
Master Election Day in Cairo: Complete Learning Path
The "Election Day" module on kodikra.com is a foundational learning path designed to teach you how to manage and manipulate core data structures in Cairo. You will master structs, arrays, and key-value maps by building a practical vote-counting system, a critical skill for any StarkNet developer.
You’ve just been handed a digital ballot box, overflowing with thousands of votes. Your task is to declare a winner, and do it fast, transparently, and verifiably on a blockchain. Manually sifting through this data is impossible, and a single mistake could undermine the entire election. This feeling of needing to organize complex, unstructured data into a coherent, queryable system is a core challenge every developer faces, especially in the rigid and resource-sensitive environment of smart contracts.
This is where the Cairo programming language offers powerful tools, but its strictness can be intimidating. How do you group related information? How do you handle a list of items that can grow? What’s the most efficient way to count unique entries? This guide is your answer. We will walk you through the essential Cairo data structures—structs, Array<T>, and LegacyMap<K, V>—using the practical and engaging "Election Day" scenario from the exclusive kodikra.com curriculum. By the end, you won't just know the theory; you'll have the hands-on experience to confidently manage complex state in your own StarkNet applications.
What is the "Election Day" Module? The Foundation of Data Management
At its heart, the Election Day module is not just about politics or voting; it's a masterclass in data organization and manipulation. It uses the familiar concept of an election to teach you the three pillars of data structuring in Cairo. Think of it as your bootcamp for handling collections of information, a skill that is non-negotiable for building anything meaningful on StarkNet, from DeFi protocols to NFT marketplaces.
This module forces you to think like a smart contract engineer, where every operation has a cost and data layout directly impacts performance and gas fees. You will learn to model real-world entities, store them in dynamic lists, and then aggregate that data into efficient, searchable formats. Successfully completing this module on the Cairo Learning Roadmap signals a crucial transition from understanding basic syntax to building logical, stateful applications.
The primary concepts you will master are:
- Custom Data Types with
structs: Learn to bundle different pieces of data into a single, logical unit, like creating aCandidateprofile or aVoterecord. - Dynamic Lists with
Array<T>: Understand how to manage collections of items that can grow or change, such as a list of all cast votes. - Efficient Lookups with
LegacyMap<K, V>: Discover the power of key-value stores for tasks like tallying votes, where you need to quickly access and update a specific candidate's vote count.
The First Building Block: Defining Custom Types with Structs
What is a Struct in Cairo?
A struct, short for structure, is a custom data type that lets you group together multiple related values into a single, meaningful unit. Instead of passing around separate variables for a candidate's name, ID, and party, you can encapsulate all of that information into one Candidate struct. This makes your code dramatically cleaner, more readable, and less error-prone.
In Cairo, structs are the primary way to model the real-world objects and concepts your smart contract needs to interact with. They are the blueprints for the data that defines your application's state.
Why Are Structs Essential?
Imagine trying to manage an election with loose variables: candidate1_name, candidate1_votes, candidate2_name, candidate2_votes, and so on. It's a recipe for disaster. Structs solve this by creating a clean, reusable template. This approach promotes code organization and type safety, as the Cairo compiler will ensure you are always working with the correct data structure.
Furthermore, when you need to store complex data in your contract's state or pass it between functions, using a struct is far more efficient and manageable than juggling a dozen individual parameters.
How to Define and Use Structs in Cairo
Defining a struct is straightforward. You use the struct keyword, followed by the name of your struct, and then a list of its fields, each with a name and a type. The #[derive(Drop, Serde)] attributes are often added to allow the compiler to automatically generate code for memory management (Drop) and serialization/deserialization (Serde), which is crucial for storage.
Here’s how you might define structs for candidates and votes in our election scenario:
use starknet::ContractAddress;
// #[derive] tells the compiler to auto-generate common trait implementations.
// Drop is for memory management.
// Serde is for serialization, essential for storing structs.
#[derive(Copy, Drop, Serde)]
struct Candidate {
name: felt252,
party: felt252,
}
#[derive(Copy, Drop, Serde)]
struct Vote {
voter_address: ContractAddress,
candidate_name: felt252,
timestamp: u64,
}
// Example of creating an instance of a struct
fn create_sample_vote() -> Vote {
let candidate_name = 'Candidate A';
let voter = starknet::get_caller_address();
Vote {
voter_address: voter,
candidate_name: candidate_name,
timestamp: starknet::get_block_timestamp(),
}
}
In this example, Candidate and Vote are now new types in our program. We can create instances of them and access their fields using dot notation (e.g., my_vote.candidate_name), making our logic clear and intuitive.
Handling Collections: Managing Lists with `Array`
What is an `Array`?
An Array<T> in Cairo is a dynamic array, which is a collection of elements of the same type (specified by T). Unlike fixed-size arrays in some other languages, a Cairo Array can grow as you add more elements, making it perfect for situations where you don't know the final size of your data collection beforehand, like receiving an unknown number of votes.
It stores elements contiguously in memory, which provides certain performance characteristics. It's the go-to data structure for managing ordered lists of items.
Why is `Array` a Core Tool?
In our Election Day scenario, the most natural way to represent the stream of incoming ballots is as a list. An Array<Vote> is the perfect tool for this job. It allows us to collect every single vote cast in the order they are received (or in any order) before we begin the tallying process.
Arrays are fundamental for any task that involves iterating over a set of items, such as displaying a list of registered candidates, processing a batch of transactions, or, in our case, examining each vote one by one.
How to Work with Arrays in Cairo
The Cairo core library provides the ArrayTrait to interact with arrays. You can create a new array, append elements to it, get its length, and access elements by index. Remember that arrays are mutable, so you must declare them with the let mut keyword if you intend to add elements.
Here’s a practical example of collecting votes into an array:
use array::ArrayTrait;
// Assuming the Vote struct from the previous section is defined
fn collect_votes() -> Array<Vote> {
// Initialize a new, empty array that will hold Vote structs.
let mut all_votes = ArrayTrait::new();
// Simulate receiving three votes
let vote1 = Vote {
voter_address: 0x1.try_into().unwrap(),
candidate_name: 'Candidate A',
timestamp: 100
};
let vote2 = Vote {
voter_address: 0x2.try_into().unwrap(),
candidate_name: 'Candidate B',
timestamp: 101
};
let vote3 = Vote {
voter_address: 0x3.try_into().unwrap(),
candidate_name: 'Candidate A',
timestamp: 102
};
// Append each vote to the end of the array.
all_votes.append(vote1);
all_votes.append(vote2);
all_votes.append(vote3);
// The array now contains [vote1, vote2, vote3]
all_votes
}
A critical concept when iterating is the use of span(), which provides an immutable view of the array's elements at a specific moment. This is a key safety feature in Cairo.
● Start: Receive Raw Vote Data
│
▼
┌───────────────────┐
│ Instantiate Vote │
│ struct from data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ let mut votes = │
│ ArrayTrait::new() │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ votes.append( │
│ new_vote_struct │
│ ) │
└─────────┬─────────┘
│
▼
◆ More Votes?
╱ ╲
Yes No
│ │
▼ ▼
Loop Back [Array is Ready]
│ │
└────────┬──────┘
▼
● End: Votes Collected
The Power of Key-Value Pairs: Efficient Tallying with `LegacyMap`
What is a `LegacyMap`?
A LegacyMap<K, V> is Cairo's implementation of a hash map or dictionary. It's a powerful data structure that stores data as key-value pairs. For any given key (K), you can store and retrieve an associated value (V). The magic of a hash map is its speed: retrieving a value for a specific key is, on average, an O(1) operation, meaning it takes roughly the same amount of time regardless of how many items are in the map.
Note: The name `LegacyMap` indicates it's an older implementation primarily used for storage variables. Newer Cairo versions introduce more modern dictionary structures for in-memory operations, but LegacyMap remains a vital tool for on-chain state management, which is exactly what we need for an election.
Why is it Perfect for Vote Counting?
We have our votes collected in an Array<Vote>. Now, how do we count them? We could iterate through the array for each candidate, but that would be incredibly inefficient. If we have 10 candidates and 1 million votes, that's 10 million operations!
A LegacyMap provides a far superior solution. We can use the candidate's name (a felt252) as the key and their vote count (a u32) as the value. As we iterate through our array of votes, we simply read the current count for the candidate, increment it by one, and write it back. This is lightning-fast and scales beautifully.
How to Implement a Vote Tally
To use a LegacyMap, you need to define it within your contract's storage. It's not typically used as a temporary variable inside a function. The LegacyMapTrait provides the read and write methods to interact with it.
use starknet::ContractAddress;
use array::ArrayTrait;
#[starknet::interface]
trait IElection<TContractState> {
fn cast_vote(ref self: TContractState, candidate_name: felt252);
fn get_vote_count(self: @TContractState, candidate_name: felt252) -> u32;
}
#[starknet::contract]
mod Election {
use super::{Vote, Candidate}; // Assuming these structs are defined elsewhere
#[storage]
struct Storage {
// Here we define our key-value map in the contract's storage.
// Key: felt252 (Candidate Name)
// Value: u32 (Vote Count)
vote_counts: LegacyMap<felt252, u32>,
}
#[abi(embed_v0)]
impl ElectionImpl of super::IElection<ContractState> {
fn cast_vote(ref self: ContractState, candidate_name: felt252) {
// 1. Read the current vote count for the given candidate.
let current_count = self.vote_counts.read(candidate_name);
// 2. Increment the count.
let new_count = current_count + 1;
// 3. Write the new count back to the map.
self.vote_counts.write(candidate_name, new_count);
}
fn get_vote_count(self: @ContractState, candidate_name: felt252) -> u32 {
// Public function to read the tally for any candidate.
self.vote_counts.read(candidate_name)
}
}
}
Pros & Cons: `Array` vs. `LegacyMap` for Tallying
Choosing the right data structure is critical for performance and cost. Here’s a comparison for the specific task of counting votes.
| Aspect | Array<T> (for storing raw votes) |
LegacyMap<K, V> (for storing tallies) |
|---|---|---|
| Primary Use Case | Storing an ordered or unordered collection of individual items. | Associating a unique key with a specific value for fast lookups. |
| Lookup Speed | Slow (O(n)). To find a vote or count, you must potentially scan the entire array. | Extremely Fast (O(1) on average). You can directly access a candidate's count. |
| Gas Cost (Storage) | Cost grows linearly with each vote appended. Storing every single vote on-chain can be very expensive. | Cost grows only with the number of unique candidates. Each new vote for an existing candidate is just an update (a single `SSTORE` operation). |
| Data Ordering | Preserves the order of insertion. Useful if you need to know the sequence of votes. | Unordered. A hash map does not guarantee any specific order of its keys. |
| Best For... | Collecting raw data, logging events, maintaining a historical record. | Aggregating data, counting frequencies, storing balances, managing permissions. |
Putting It All Together: The Election Day Challenge Logic
From a List of Votes to a Clear Winner
The core task of the Election Day module is to combine these concepts into a cohesive algorithm. While the previous example showed writing directly to storage, a common pattern involves processing a batch of votes collected in an array and updating the map accordingly. The final step is to find the key (candidate) with the highest value (vote count).
The algorithm looks like this:
- Data Ingestion: Receive a list of votes, likely as an
Array<Vote>passed to a function. - Tallying Phase: Iterate through the
Array<Vote>. For eachVotestruct, extract thecandidate_nameand update its count in theLegacyMap<felt252, u32>. - Winner Determination: To find the winner, you need a list of all candidates. Iterate through this list, read the final count for each from the
LegacyMap, and keep track of the candidate with the highest count seen so far.
● Start: Tally Complete
│
▼
┌──────────────────┐
│ Initialize │
│ `winner = None` │
│ `max_votes = 0` │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Get List of All │
│ Candidates │
└────────┬─────────┘
│
▼
Iterate through Candidates
│
▼
┌──────────────────┐
│ Read `count` for │
│ current candidate│
│ from `vote_counts` map │
└────────┬─────────┘
│
▼
◆ count > max_votes?
╱ ╲
Yes No
│ │
▼ ▼
┌─────────────────┐ (Continue Loop)
│ Update `winner` │ │
│ Update `max_votes`│ │
└────────┬────────┘ │
│ │
└────────┬───────┘
▼
(End of Loop)
│
▼
● Announce `winner`
Common Pitfalls and How to Avoid Them
- Forgetting
let mut: If you need to change a variable (like an array you're appending to or a counter), you must declare it as mutable. The compiler will stop you otherwise. - Gas-Heavy Loops: Every write to storage (like `self.vote_counts.write()`) costs a significant amount of gas. Performing storage writes inside a loop that runs many times can make your function prohibitively expensive. It's often better to process data in memory and write the final result to storage once.
- Handling Zero/Default Values: When you read from a `LegacyMap` for a key that has never been written to, it returns the default value for the type (e.g.,
0for au32). Your logic must correctly handle this initial state. - Ownership and Snapshots: When you iterate over an array using `span()`, you get an immutable "snapshot." You cannot modify the original array while you are iterating over its snapshot. This is a core safety feature of Cairo's ownership model that you must understand.
Your Learning Path: The Kodikra Module
This entire body of theory is put into practice in the kodikra.com learning module. You will be given a boilerplate contract and a set of objectives to implement the logic we've discussed. This hands-on experience is the fastest way to internalize these critical concepts.
The module will guide you through building functions to tally votes, find winning candidates, and manage election data efficiently. It's the perfect environment to make mistakes, learn from the compiler, and solidify your understanding.
Beyond the Module: Real-World Applications
The skills you learn in the Election Day module are not academic. They are the building blocks for countless real-world blockchain applications:
- DeFi Protocols: Using maps to track user balances (
LegacyMap<ContractAddress, u256>). - DAO Governance: Storing proposals in structs and tallying votes from token holders.
- NFT Marketplaces: Using a map to track the owner of each NFT ID (
LegacyMap<u256, ContractAddress>). - On-Chain Gaming: Storing player profiles or game items in structs and tracking high scores on a leaderboard map.
- Identity Systems: Associating a user's address with an on-chain profile struct.
Frequently Asked Questions (FAQ)
What's the difference between an `Array` and a tuple in Cairo?
An Array<T> is a dynamic collection where all elements must be of the same type T, and its size can change at runtime. A tuple, like (u32, felt252), is a fixed-size, ordered list of values where each value can have a different type. Tuples cannot grow or shrink.
Why is `LegacyMap` called "legacy"? Are there better alternatives?
It's named "legacy" because it was the primary mapping structure in early versions of Cairo and is specifically designed to work with contract storage. For in-memory computations within a single function call, newer Cairo versions offer more flexible dictionary traits and types. However, for persistent on-chain key-value state, LegacyMap remains the standard and essential tool.
How do I handle strings or short strings (`felt252`) as keys in a map?
A felt252 is the standard way to represent short strings in Cairo and works perfectly as a key in a LegacyMap. The `felt252` is a 252-bit field element, which can hold up to 31 ASCII characters. This is what we use for `candidate_name` in the Election Day examples.
What does `#[derive(Drop)]` on a struct actually do?
The Drop trait tells the compiler how to handle the memory deallocation for a type when it goes out of scope. By using #[derive(Drop)], you instruct the compiler to automatically generate this boilerplate code for your struct, ensuring its memory is properly cleaned up. This is crucial for preventing memory leaks and is required for most custom types.
How can I optimize my loops for gas efficiency in Cairo?
The golden rule is to minimize storage access (reads and especially writes) inside loops. If possible, load data from storage into memory, perform all your computations, and then write the final results back to storage once. Avoid patterns that require writing to storage on every single iteration of a large loop.
Can a `struct` contain another `struct` or an `Array`?
Yes, absolutely. This is a common and powerful pattern called composition. For example, an Election struct could contain a list of all candidates: struct Election { candidates: Array<Candidate>, start_time: u64 }. This allows you to build complex, hierarchical data models.
What is the best way to debug issues with data structures in Cairo?
The best approach is to write comprehensive unit tests using a framework like Starknet Foundry. In your tests, you can create instances of your structs and arrays, call your functions, and then assert that the final state of your data structures is what you expect. You can also emit events from your contract at key points to log the state of variables during a transaction.
Conclusion: Your Next Step to Cairo Mastery
You have now explored the theory behind three of the most important concepts in Cairo programming: organizing data with structs, collecting it with Array<T>, and aggregating it efficiently with LegacyMap<K, V>. The Election Day module is more than just an exercise; it's a microcosm of the challenges you will face when building any stateful smart contract on StarkNet. Mastering this module is a significant step toward becoming a proficient Cairo developer.
The path to expertise is through practice. Take this knowledge and apply it. Dive into the kodikra.com module, write the code, run the tests, and see these data structures come to life.
Technology Disclaimer: The Cairo language and the StarkNet ecosystem are under continuous development. While the core concepts presented here are stable, syntax and best practices may evolve. Always refer to the official Cairo and StarkNet documentation for the most current information.
Ready to build? Start the challenge and cast your first vote.
Published by Kodikra — Your trusted Cairo learning resource.
Post a Comment