Starknet Byte Directory: A Simple Implementation Selector Directory

Starknet is a validity rollup (Layer 2) built on Ethereum to help scale the network. Unlike Ethereum, which uses Solidity for smart contract development, Starknet leverages Cairo, a programming language designed to enable provable computation using STARKs (Succinct Transparent Arguments of Knowledge). This article explores the concept of function selectors in Starknet, compares it to Ethereum's approach, and demonstrates a practical implementation of a Starknet byte directory.


Function Selectors: An Overview

What is a Function Selector?

A function selector is a unique identifier that specifies which function in a smart contract should be executed. In Ethereum, this is typically a 4-byte value derived from the function signature. On Starknet, the concept is similar but implemented differently due to Cairo's unique characteristics.


Function Selectors in Solidity (Ethereum)

In Solidity, function selectors are generated using the function signature, which includes the function name and parameter types. The selector is computed as the first 4 bytes of the Keccak-256 hash of the function signature.

Example: Generating a Function Selector in Solidity

function point(uint256 x, uint256 y) external {}

// Generating the function selector:
bytes4(keccak256(abi.encodePacked("point(uint256,uint256)")));

Here, the function signature point(uint256,uint256) is hashed using Keccak-256, and the first 4 bytes of the hash become the function selector.


Function Selectors in Cairo (Starknet)

Cairo, Starknet's programming language, takes a different approach to function selectors. Unlike Solidity, Cairo does not support function overloading, so the selector is derived solely from the function name without including parameter types.

Generating Function Selectors in Cairo

Starknet provides a utility function called get_selector_from_name to compute the selector. This function uses a variant of Keccak-256 called starknet_keccak, which ensures the result fits within a Starknet field element.

Code Example:

pub fn get_selector_from_name(func_name: &str) -> Result<Felt, NonAsciiNameError> {
    if func_name == DEFAULT_ENTRY_POINT_NAME || func_name == DEFAULT_L1_ENTRY_POINT_NAME {
        Ok(Felt::ZERO)
    } else {
        let name_bytes = func_name.as_bytes();
        if name_bytes.is_ascii() {
            Ok(starknet_keccak(name_bytes))
        } else {
            Err(NonAsciiNameError)
        }
    }
}

pub fn starknet_keccak(data: &[u8]) -> Felt {
    let mut hasher = Keccak256::new();
    hasher.update(data);
    let mut hash = hasher.finalize();

    // Remove the first 6 bits to fit within a Starknet field element
    hash[0] &= 0b00000011;

    // Convert the hash to a field element
    Felt::from_bytes_be(unsafe { &*(hash[..].as_ptr() as *const [u8; 32]) })
}

Key Differences:

  1. No Parameter Types: Cairo selectors are derived only from the function name.

  2. Field Element Compatibility: The starknet_keccak function ensures the result fits within a Starknet field element by modifying the first 6 bits of the hash.


Starknet Byte4Directory Implementation

To create a robust database for function selectors in the Starknet ecosystem, we can implement a Starknet Byte4Directory. This directory stores function names, their corresponding selectors, and the first 4 bytes of the selector for easy reference.

Implementation Details

The implementation involves:

  1. Generating the function selector using get_selector_from_name.

  2. Storing the selector and its metadata in a database.

  3. Providing an API endpoint to submit and retrieve function selectors.

Code Example: /submit Endpoint

#[post("/submit")]
async fn submit(req_body: web::Json<FunctionNameRequest>) -> impl Responder {
    let mut connection = db_connect();
    let func_name = req_body.function_name.clone();

    // Check if the function name already exists in the database
    match selectors::table
        .filter(selectors::function_name.eq(&func_name))
        .first::<CreateSelector>(&mut connection)
    {
        Ok(_) => {
            // Function name already exists, return an error
            return HttpResponse::BadRequest().body("Error: Function name already exists");
        }
        Err(diesel::result::Error::NotFound) => {
            // Function name does not exist, proceed to insert
            match get_selector_from_name(&func_name) {
                Ok(felt) => {
                    let felt_hex = format!("{:#x}", felt);
                    let first_4_bytes = &felt_hex[0..10];
                    let new_data = CreateSelector {
                        id: Uuid::new_v4(),
                        function_name: func_name.clone(),
                        felt_selector: felt_hex.clone(),
                        selector: first_4_bytes.to_string(),
                    };

                    // Insert the new data into the database
                    diesel::insert_into(selectors::table)
                        .values(&new_data)
                        .execute(&mut connection)
                        .expect("Error inserting new selector");

                    HttpResponse::Ok().json(new_data)
                }
                Err(e) => HttpResponse::BadRequest().body(format!("Error: {:?}", e)),
            }
        }
        Err(e) => {
            HttpResponse::InternalServerError().body(format!("Database error: {:?}", e))
        }
    }
}

How It Works:

  1. The /submit endpoint accepts a function name as input.

  2. It checks if the function name already exists in the database.

  3. If the name is unique, it generates the selector using get_selector_from_name, extracts the first 4 bytes, and stores the data in the database.

  4. If the name already exists, it returns an error.


Why Cairo Doesn't Support Function Overloading

Cairo's design prioritizes simplicity and efficiency for STARK proofs. Function overloading, which allows multiple functions with the same name but different parameter types, is not supported because:

  1. Determinism: STARK proofs require deterministic computation, and overloading introduces ambiguity.

  2. Simplicity: Avoiding overloading simplifies the compiler and runtime, making generating and verifying proofs easier.


Benefits of a Starknet Byte Directory

A Starknet byte directory provides several benefits:

  1. Centralized Reference: Developers can quickly look up function selectors for their contracts.

  2. Efficiency: Reduces the need to recompute selectors, saving computational resources.


Repository and Further Exploration

To explore the implementation in detail, check out the repository here


Summary Table: Solidity vs. Cairo Function Selectors

FeatureSolidity (Ethereum)Cairo (Starknet)
Selector Generationbytes4(keccak256(abi.encodePacked(...)))get_selector_from_name(func_name)
Includes Parameter TypesYesNo
Function OverloadingSupportedNot Supported
Hash FunctionKeccak-256Starknet Keccak

Conclusion

The Starknet byte directory is a tool for managing function selectors in the Starknet ecosystem. The implementation is a starting point for creating a robust function selector directory.

Repositories:

Backend: https://github.com/codeWhizperer/bytedirectory

Frontend: https://github.com/codeWhizperer/byteFrontend