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 (public key) to a balance.
@storage_var
func balance(user : felt) -> (res : felt):
end

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{
        storage_ptr : Storage*, range_check_ptr,
        pedersen_ptr : HashBuiltin*}(
    user : felt) -> (res : felt)

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

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

Signature verification

We now have to modify increase_balance to do the following:

  1. Write to the appropriate balance entry.

  2. Verify that the user has signed on this change.

For the signature, we will use the STARK-friendly ECDSA signature, which is natively supported in Cairo. For technical details about this cryptographic primitive see STARK Curve.

We will need the ecdsa builtin to verify the signature, so we will change the %builtins line to:

%builtins pedersen range_check ecdsa

and add the following import statement:

from starkware.cairo.common.cairo_builtins import (
    SignatureBuiltin)
from starkware.cairo.common.signature import (
    verify_ecdsa_signature)

Next, we will change the code of increase_balance() to:

# Increases the balance of the given user by the given amount.
@external
func increase_balance{
        storage_ptr : Storage*, pedersen_ptr : HashBuiltin*,
        range_check_ptr, ecdsa_ptr : SignatureBuiltin*}(
        user : felt, amount : felt, sig_r : felt, sig_s : felt):
    # Verify the user's signature.
    verify_ecdsa_signature(
        message=amount,
        public_key=user,
        signature_r=sig_r,
        signature_s=sig_s)

    let (res) = balance.read(user=user)
    balance.write(user, res + amount)
    return ()
end

verify_ecdsa_signature behaves like an assert – in case the signature is invalid, the function will revert the entire transaction.

Note that we don’t handle replay attacks here – once the user signs a transaction someone may call it multiple times. One way to prevent replay attacks is to add a nonce argument to increase_balance, change the signed message to the Pedersen hash of the nonce and the amount and define another storage map from the signed message to a flag (either 0 or 1) indicating whether or not that transaction was executed by the system. Future versions of StarkNet will handle user authentication and prevent replay attack.

Similarly, change the code of get_balance(). Here we don’t need to verify the signature (since StarkNet’s storage is not private anyway), so the change is simpler:

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

Compile and deploy

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

Compile and deploy the file:

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

starknet deploy --contract user_auth_compiled.json

Don’t forget to set STARKNET_NETWORK=alpha before running starknet deploy.

Interacting with the contract

First, we need to generate a pair of public and private keys. We will use a constant private key (of course, in a real application choosing a secure random private key is imperative). Then, we sign a message to increase the balance by 4321. For this, we will use the following python statements:

from starkware.crypto.signature.signature import (
    pedersen_hash, private_to_stark_key, sign)
private_key = 12345
public_key = private_to_stark_key(private_key)
print(f'Public key: {public_key}')
print(f'Signature: {sign(msg_hash=4321, priv_key=private_key)}')

You should get:

Public key: 1628448741648245036800002906075225705100596136133912895015035902954123957052
Signature: (2620967193230873397198710803425457084022525354559824107385923461037870205486, 3272947357463083975342237526788619260723986710381984701548320822682741741637)

Now, let’s update the balance:

starknet invoke \
    --address CONTRACT_ADDRESS \
    --abi user_auth_abi.json \
    --function increase_balance \
    --inputs \
        1628448741648245036800002906075225705100596136133912895015035902954123957052 \
        4321 \
        2620967193230873397198710803425457084022525354559824107385923461037870205486 \
        3272947357463083975342237526788619260723986710381984701548320822682741741637

You can query the transaction status:

starknet tx_status --id=TX_ID

Finally, after the transaction is executed (status PENDING or ACCEPTED_ONCHAIN) we may query the user’s balance.

starknet call \
    --address CONTRACT_ADDRESS \
    --abi user_auth_abi.json \
    --function get_balance \
    --inputs 1628448741648245036800002906075225705100596136133912895015035902954123957052

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 = 1628448741648245036800002906075225705100596136133912895015035902954123957052
user_balance_key = get_storage_var_address('balance', user)
print(f'Storage key for user {user}:\n{user_balance_key}')

You should get:

Storage key for user 1628448741648245036800002906075225705100596136133912895015035902954123957052:
142452623821144136554572927896792266630776240502820879601186867231282346767