Adding user authentication

Storage maps

Suppose that instead of maintaining one global variable balance, we would like to have a balance for each user (users will be identified by their STARK public keys).

Our first task will be to change the balance storage variable to a map from public key (user) to balance (instead of a single value). This can be done by simply adding an argument:

// A map from user (represented by account contract address)
// to their balance.
@storage_var
func balance(user: felt) -> (res: felt) {
}

In fact, the @storage_var decorator allows you to add multiple arguments to create even more complicated maps. The functions balance.read() and balance.write() will now have the following signatures:

func read{
    syscall_ptr: felt*,
    range_check_ptr,
    pedersen_ptr: HashBuiltin*,
}(user: felt) -> (res: felt) {
}

func write{
    syscall_ptr: felt*,
    range_check_ptr,
    pedersen_ptr: HashBuiltin*,
}(user: felt, value: felt) {
}

Note that the default value of all the entries in the map is 0.

Getting the caller address

In order to obtain the address of the account contract (or any other contract, in the case that the function was invoked by a contract) that invoked our function, we can use the get_caller_address() library function:

from starkware.starknet.common.syscalls import get_caller_address

// ...

let (caller_address) = get_caller_address();

get_caller_address() returns the address of the source contract that called this contract. It can be the address of the account contract or the address of another contract (if the function was invoked by another contract). When the contract is called directly (rather than through a contract), the function returns 0.

Note that if you use get_caller_address() in a function foo() that was called by another function bar() within your contract, it will still return the address of the contract that invoked bar() (or 0 if it was invoked directly).

Modifying the contract’s functions

Change the code of increase_balance() to:

from starkware.cairo.common.math import assert_nn

// Increases the balance of the user by the given amount.
@external
func increase_balance{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr,
}(amount: felt) {
    // Verify that the amount is positive.
    with_attr error_message(
            "Amount must be positive. Got: {amount}.") {
        assert_nn(amount);
    }

    // Obtain the address of the account contract.
    let (user) = get_caller_address();

    // Read and update its balance.
    let (res) = balance.read(user=user);
    balance.write(user, res + amount);
    return ();
}

Note that we added a constraint that the value of amount must be nonnegative, by calling assert_nn. In order to obtain an indicative message in case of an error, we wrapped the function call with the with_attr error_message(...) block. See Retrieving the revert reason for more details.

Similarly, change the code of get_balance(). Here we chose to allow the caller to query any user (since StarkNet’s storage is not private anyway):

// Returns the balance of the given user.
@view
func get_balance{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr,
}(user: felt) -> (res: felt) {
    let (res) = balance.read(user=user);
    return (res=res);
}

Compile and deploy

Save the new contract file as user_auth.cairo. You can find the full Cairo file here.

Compile and declare the contract:

starknet-compile user_auth.cairo \
    --output user_auth_compiled.json \
    --abi user_auth_abi.json
starknet declare --contract user_auth_compiled.json

Deploy the contract:

starknet deploy --class_hash ${USER_AUTH_CLASS_HASH}

where ${USER_AUTH_CLASS_HASH} is the value of class_hash. Don’t forget to set the STARKNET_NETWORK and STARKNET_WALLET environment variables and deploy an account contract before running starknet deploy.

Interacting with the contract

Let’s update the balance:

starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi user_auth_abi.json \
    --function increase_balance \
    --inputs 4321

You can query the transaction status:

starknet tx_status --hash TX_HASH

Finally, after the transaction is executed (status ACCEPTED_ON_L2 or ACCEPTED_ON_L1) you may query the user’s balance:

starknet call \
    --address ${CONTRACT_ADDRESS} \
    --abi user_auth_abi.json \
    --function get_balance \
    --inputs ${ACCOUNT_ADDRESS}

You should get:

4321

Note that if you want to use the get_storage_at CLI command to query the balance of a specific user, you can no longer compute the relevant key by only supplying the name of the storage variable. That is because the balance storage variable now requires an additional argument, namely, the user key. Hence, you will need to supply the additional arguments when acquiring the key used in get_storage_at. In our case, this translates to the following Python code:

from starkware.starknet.public.abi import get_storage_var_address

user = ACCOUNT_ADDRESS
user_balance_key = get_storage_var_address('balance', user)
print(f'Storage key for user {user}:\n{user_balance_key}')

Retrieving the revert reason

Let’s try to invoke increase_balance with a negative amount:

starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi user_auth_abi.json \
    --function increase_balance \
    --inputs -1000

Because this transaction is invalid (as the amount is negative), you will get an error from the StarkNet gateway that contains the following:

{"code": "StarknetErrorCode.TRANSACTION_FAILED", "message": "Error at pc=0:38:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:522)\nUnknown location (pc=0:484)\nUnknown location (pc=0:590)\n\nError in the called contract (0x548245153813267af2d2793c6e5d60c40cb95f34d7404f2ce75550fafabede0):\nError at pc=0:6:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:155)\nError message: Amount must be positive. Got: -1000.\nUnknown location (pc=0:129)\n\nTraceback (most recent call last):\n  File \"<hint0>\", line 3, in <module>\nAssertionError: a = 3618502788666131213697322783095070105623107215331596699973092056135872019481 is out of range."}

This indicates that the CLI could not estimate the transaction fee, because the transaction has failed. For the sake of demonstrating retrieving the revert reason, we will force the transaction to skip the fee estimation mechanism. To do so, add --max_fee 100000000000000000 to the former invoke transaction, as follows:

starknet invoke \
    --address ${CONTRACT_ADDRESS} \
    --abi user_auth_abi.json \
    --function increase_balance \
    --inputs -1000 \
    --max_fee 100000000000000000

After this, when querying the transaction status, you should get:

{
    "tx_failure_reason": {
        "code": "TRANSACTION_FAILED",
        "error_message": "Error at pc=0:32:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:494)\nUnknown location (pc=0:453)\nUnknown location (pc=0:510)\n\nError in the called contract (0x3632c8d1265888e0eadb518cbf4a83d071d00cd8f946ec72fd661e69eea1963):\nError at pc=0:6:\nGot an exception while executing a hint.\nCairo traceback (most recent call last):\nUnknown location (pc=0:155)\nError message: Amount must be positive. Got: -1000.\nUnknown location (pc=0:129)\n\nTraceback (most recent call last):\n  File \"<hint0>\", line 3, in <module>\nAssertionError: a = 3618502788666131213697322783095070105623107215331596699973092056135872019481 is out of range."
    },
    "tx_status": "REJECTED"
}

Notice that the error message entry states that the error location is unknown. This is because the StarkNet network is not aware of the source code and debug information of a contract. To retrieve the error location and reconstruct the traceback, add the path to the relevant compiled contract in the transaction status query, using the --contracts argument. To better display the error (and only it), add the --error_message flag as well:

starknet tx_status \
    --hash TX_HASH \
    --contracts ${CONTRACT_ADDRESS}:user_auth_compiled.json \
    --error_message

The output should look like:

Error at pc=0:28:
Got an exception while executing a hint.
Cairo traceback (most recent call last):
Unknown location (pc=0:494)
Unknown location (pc=0:453)
Unknown location (pc=0:510)

Error in the called contract (0x29cd5db92729052b3268471cf1b2327b61523565adeaa1d659236e806bd4b97):
math.cairo:47:5: Error at pc=0:6:
    a = [range_check_ptr];
    ^*******************^
Got an exception while executing a hint.
Cairo traceback (most recent call last):
user_auth.cairo:15:6
func increase_balance{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
     ^**************^
Error message: Amount must be positive. Got: -1000.
user_auth.cairo:20:9
        assert_nn(amount);
        ^***************^

Traceback (most recent call last):
  File "<hint0>", line 3, in <module>
AssertionError: a = 3618502788666131213697322783095070105623107215331596699973092056135872019481 is out of range.

You should ignore the first part (before Error in the called contract) – it is caused by the account contract.