DOFY's Blog
DOFY's Blog

How Cairo Virtual Machine delegates contract function invocations internally

Key concepts of contract function invocations

Function Selector

In general, the function selector in StarkNet is used to specify a contract function. It is a hash of the function name, which is defined as the last 250 bits of the Keccak256 hash of the name encoded in ASCII. The reason it only takes the last 250 bits is probably to make the value fit in a StarkNet field element, because P=2^{251}+17\cdot 2^{192}+1. In the implementation, the Cairo VM get the selector by calling get_selector_from_name().

Unlike EVM, the selector in StarkNet is just a hash of the function name, rather than a hash of the function signature, i.e. the parameter types will not be considered when computing the selector. Only the name is depended on.

For example, the keccak256 hash of mint is daf0b3c5710379609eb5495f1ecd348cb28167711b73609fe565a72734550354. So its selector will be 2f0b3c5710379609eb5495f1ecd348cb28167711b73609fe565a72734550354.

calldata

The calldata in StarkNet is the encoding of arguments for a function call. When users call *.invoke(), the Cairo VM will first construct calldata and pass it to the contract function for execution.

How does Cairo VM constructs calldata?

The calldata in StarkNet is a list of int. The way of argument encoding depends on its type. The following elementary types exist in StarkNet:

  1. felt: a field element
  2. Struct: a user-defined data structure
  3. Tuple: named or unnamed
  4. Pointer: with a pointee type

Details of these types can be found at here and here.

For an argument with type felt, Cairo VM encodes it as an int directly, and appends it to calldata.

For an argument with type Struct/Tuple (actually, a struct can be seen as a named tuple), Cairo VM first flattens it into a list of int, then concatenates it at the end of calldata.

For an argument with type Pointer, because the syntax of StarkNet contract required that the size of the space allocated to the pointer should be provided preceding the pointer itself. That means if xxx: T* is in the function signature, then xxx_len: felt must be in the signature too. So a pointer can be seen as a fixed-size array. Therefore, Cairo VM first computed the length of the array and appends it to calldata, followed by the encoding of each element in the array.

Finally, all integers in calldata will be casted into felt (i.e. by modulo the prime P).

Examples

Here are some examples to illustrate the way Cairo VM constructs calldata.

We define a struct called MyStruct and a function foo with two arguments a and b, with type felt and MyStruct respectively.

struct MyStruct:
    member first : felt
    member second : felt
end

@external
func foo{   
    syscall_ptr : felt*,
    pedersen_ptr : HashBuiltin*,
    range_check_ptr,
}(a: felt, b: MyStruct):
    return () 
end

If we invoke the foo by contract.foo(a=100, b=(0, 1)).invoke(), the calldata will be a list [100, 0, 1].

Here is another example with tuples and pointers:

@external
func fptuple{   
    syscall_ptr : felt*,
    pedersen_ptr : HashBuiltin*,
    range_check_ptr,
}(ptuple_len:felt, ptuple: (felt,(felt,felt))*):
    return ()
end

If we invoke the fptuple by contract.fptuple(ptuple=[(1, (1, 1)), (2, (2, 2))]).invoke(), the calldata will be a list [2, 1, 1, 1, 2, 2, 2]. The first 2 is the length of the array. And the following six elements is the two tuples after flattening.

Details of Cairo VM implementation

Starknet: A high level interface to a StarkNet state Object
StarknetContract: A high level interface to a StarkNet contract used for testing. Allows invoking functions.
StarknetState: Represents a state of a StarkNet network.

InternalTransaction(InternalStateTransaction,...): StarkNet internal transaction base class. Since there are many kinds of transaction, the class also has many subclasses, including:
InternalDeclare: The declaration of a Cairo contract class.
InternalDeploy: The deployment of a Cairo contract.
InternalInvokeFunction: The invocation of a Cairo contract function.

InternalStateTransaction: StarkNet internal state transaction. The super class of InternalTransaction.
It has an important abstract method _apply_specific_state_updates(). The main role of this method is to apply different types of changes to current state according to different transaction types. The abstract method should be implemented by each concrete transaction subclass.

How does the cairo-vm work internally when calling the starknet.deploy()?

In general, the cairo-vm first compiles the contract source file and constructs the contract class, in which there is information such as ABI, program bytecodes, function entrypoints and so on. After that it calculates the contract address. Finally, it updates the state.

What does deploy() in the class Starknet do?

The method deploy() deploys a contract on StartNet and returns a StarknetContract instance which can be used to invoke contract functions. The specific workflow is as follows:

  1. get_contract_class(): Given either a ContractClass instance or a source file path (e.g. .../contract.cairo), returns the respective ContractClass instance.
    • Get the ContractClass instance through compiling the source file compile_starknet_files().
  2. self.state.deploy(): Deploys a contract. Returns the contract address and the execution info.
    • InternalDeploy.create_for_testing(): It creates an instance of deployment transaction called tx.
      • InternalDeploy.create():
        1. Computes the hash of contract class.
        2. Calculates the contract address in StartNet from the previous hash.
        3. Calculates the transaction hash of this deployment.
    • tx.apply_state_updates(): Update the state
      • To know about the details of the updating we can see the implementation of _apply_specific_state_updates() in the class InternalDeploy:
        1. initilaize_contract_state()
        2. Updates cairo usage.
        3. invoke_constructor()
  3. Returns a StarknetContract instance with the new contract address, the abi, and the state.

How does the cairo-vm work internally when calling the *.invoke() function of a deployed contract?

Generally speaking, after a contract is deployed, when users call a contract function first time, the cairo-vm will build a function object that acts as a proxy for this function. When calling *.invoke(), it will uses the proxy object to create a internal transaction which represents the invocations. The execution of the function occurs during the process of updating the transaction to the state. In the process of execution, the cairo-vm gets the entrypoint through the function selector and then runs from the entrypoint like a normal function.

Why we can call contract functions as if they were python member functions?

The class StarknetContract implememts __getattr__() to enable users to call functions in the contract as if they were member functions of the class StarknetContract.

For example, assume contract is an instance of StarknetContract, and the contract class which belongs to has a function named foo(), the users can invoke foo with parameters a=1 by writingcontract.foo(a=1).invoke(). Here is the detailed process:

  1. Given the name foo, the method __getaddr__() first finds it is one of abi functions, then call get_contract_function(name = "foo").

  2. get_contract_function() -> Callable: Returns a function object that acts as a proxy for a StarkNet contract function.
    There is a field _contract_function in the class StarknetContract, which is a dict that stores contract functions that are already built. If foo is not in the dict, then method _build_contract_function() will be called to build it.

  3. _build_contract_function() -> Callable: Builds a function object that acts as a proxy for a StarkNet contract function.

    1. In this part, it first gets the names and types of foo‘s arguments by parsing the abi.
    2. After that, it builds Pythonic type annotations to those arguments, matching their Cairo types. For example, Cairo felt corresponds to Python int; Cairo Array corresponds Python List.
    3. It defines a function named template() as the template of contract functions. Then it refines the template() with the information of foo‘s arguments.
      • The return type of template() is StarknetContractFunctionInvocation, which represents a call to a StarkNet contract with a particular state and set of inputs. This class has a method invoke() that is exactly what will be called when writing contract.foo(a=1).invoke().
    4. Finally it returns the refined function template as a callable object.

Detailed procedure for calling *.invoke()

As mentioned before, after a contract is deployed, when users call a function in the contract, the corresponding function object will be built by get_contract_function().

After passing values of arguments to the function object, it will return a instance of StarknetContractFunctionInvocation, which is construct by _build_function_call(). Let us move into the class StarknetContractFunctionInvocation.

About the Class StarknetContractFunctionInvocation

The class Represents a call to a StarkNet contract with a particular state and set of inputs. Here are several important fields:
state: StarknetState
name: str
calldata: List[int]
– …

The Class StarknetContractFunctionInvocation has two main methods named call() and invoke(). The only difference between them is that invoke() executes the function call and applies changes on the state but call() does not apply.

The methods invoke() calls _invoke_on_given_state(), which then calls invoke_raw(). invoke_raw() has four important arguments:

  1. contract_address: a hexadecimal string or an integer representing the contract address.
  2. selector: either a function name or an integer selector for the entrypoint to invoke.
  3. calldata: a list of integers to pass as calldata to the invoked function.
  4. signature: a list of integers to pass as signature to the invoked function.

The methods invoke_raw() constructs a new transaction tx, which is an instance of class InternalInvokeFunction. The class represents the transactions that are invocations of contract functions. And it is also a subclass of InternalTransaction. Therefore, we mainly concern the implementation of abstract method _apply_specific_state_updates() in this class.

About _apply_specific_state_updates() in class InternalInvokeFunction

What it does is applying self (the contract function) to the state by executing the entry point and charging fee for it (if needed). The execution flow is:

_apply_specific_state_updates() // of class InternalInvokeFunction
    ->  execute()
        ->  execute() // of class ExecuteEntryPoint
            ->  sync_execute()
                ->  _run()
                    ->  get_contract_class()
                        _get_selected_entry_point()
                        run_from_entrypoint() // of class CairoFunctionRunner
                        ->  initialize_function_entrypoint()
                            initialize_vm()
                            run_until_pc()
                            end_run()
    ->  charge_fee()

What is the entry point selector?

The selector is a hash of the function name. It can be computed by get_selector_from_name(). And the backend of the hash is keccak hash algorithm.

The selectors of contract functions are computed during get_contract_class(), i.e. during compiling the source file. Thus, the value of the selectors can be seen in contract_compiled.json:

"entry_points_by_type": {
        "CONSTRUCTOR": [],
        "EXTERNAL": [
            {
                "offset": "0x3a",
                "selector": "0x362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320"
            },
            {
                "offset": "0x5b",
                "selector": "0x39e11d48192e4333233c7eb19d10ad67c362bb28580c604d67884c85da39695"
            }
        ],
        "L1_HANDLER": []
}

The offset is the exact position of the function’s first instruction, within the cairo contract bytecode. It is also computed during compiling.

And we can see that there are three types of functions:
CONSTRUCTOR
EXTERNAL
L1_HANDLER

How to determine the entry point given the selector?

This is done by method _get_selected_entry_point() of the class ExecuteEntryPoint. The class has a field entry_point_selector that stores the selector of the function need to be executed. In this method, given the contract class, it will match entry_point_selector with the selectors in contract and get the corresponding offset, which is the exact position of the instruction that should be called within the cairo contract bytecode.

没有标签
首页      未分类      How Cairo Virtual Machine delegates contract function invocations internally

发表评论

textsms
account_circle
email

DOFY's Blog

How Cairo Virtual Machine delegates contract function invocations internally
Key concepts of contract function invocations Function Selector In general, the function selector in StarkNet is used to specify a contract function. It is a hash of the fu…
扫描二维码继续阅读
2022-07-28