# Account Abstraction & Using Standard Contracts
Full-Stack Starknet:
* **[Part 1]** 🚧 [Getting Started in Cairo & Deploying with Nile](https://hackmd.io/@sambarnes/BJvGs0JpK)
* **[Part 2]** 🐍 [Contract Interaction with starknet.py](https://hackmd.io/@sambarnes/H1Fx7OMaF)
* **[Part 3]** 👥 [StarkNet Account Abstraction & Using Standard Contracts](https://hackmd.io/@sambarnes/rkGekNvAY) (***you are here***)
* **[Part 4]** 💽 [Local Devnet & Starknet.py's Account Capabilities](https://hackmd.io/@sambarnes/By7kitOCt)
* **[Part 5]** 🎨 [StarkNet Frontends w/ Cairopal & Argent X](https://hackmd.io/@sambarnes/HydPlH9CY)
* **[Notes]** 💰 [Contract Costs & Why Our Design Needs Work](https://hackmd.io/@sambarnes/SkxMZHhRK)
*Completed code on GitHub [here](https://github.com/sambarnes/fullstack-starknet)*.
Alright, so by now you might be seeing how writing/maintaining our own signature schemes could get pretty annoying if we have to do this everywhere. Fortunately, [a standard is emerging](https://github.com/OpenZeppelin/cairo-contracts/blob/main/docs/Account.md) for "Accounts" on StarkNet. In the future, when fees are introduced to the platform, all contract invocations will be required to be called from an Account contract (*\*citation needed\**).
## Accounts on starknet
The Account Abstraction is *somewhat similar* to the concept of accounts in Ethereum, however, there are a few fundamental differences in the approach. Primarily, that an Account contract can be rigid enough to have a static/fixed address -- its primary id -- while being flexible enough to have a dynamic/updatable signing keypair.
Although not necessary for this tutorial, I highly recommended that you come back and digest [Perama's blogpost on the topic](https://perama-v.github.io/cairo/account-abstraction/). In the linked post, a solid tl;dr of Ethereum vs Starknet Accounts:
> Tired (concrete): Check that transaction comes with a correctly signed signature
for the given address.
>
> Wired (abstract): Check that the transaction comes from the given address.
The account contract will handle the authentication of a call (i.e. checking that the signature is valid for the current public key, nonce, and calldata). Separately, our contract need only assert that the transaction comes from the address it's claiming to be from -- relying on the assumption that the user's account contract would only execute the call if signature verification succeeded.
## Forking the Account Abstraction locally
Because it's not (yet) a native functionality of the platform, we'll copy [the OpenZeppelin example implementation](https://github.com/OpenZeppelin/cairo-contracts/blob/main/contracts/Account.cairo) for now. The [OpenZeppelin/cairo-contracts](https://github.com/OpenZeppelin/cairo-contracts) repo has the (🚧 WIP) standard contracts that are being developed for devs to build & rely on. They are constantly evolving these days, though as the language matures & solidifies, so will the standards.
No package manager exists for the ecosystem yet either, so we're going to simply fork the following from that repo into our project:
```
contracts/
├── Account.cairo
├── ERC165_base.cairo
├── IAccount.cairo
├── Initializable.cairo
└── utils
├── constants.cairo
└── safemath.cairo
tests/
├── mocks
│ └── safemath_mock.cairo
├── test_Account.py
├── test_Initializable.py
├── test_safemath.py
└── utils.py
```
> NOTE: At the time of writing, we were encouraged to just copy the contracts directly with little direction on project structure.
>
> Feb 13, 2022: the cairo-contracts repo has evolved significantly. Now, we can just copy the whole `openzeppelin` folder into our repo to be used as a dependency. You may need to update the python tests below accordingly, to account for the different contract filepaths to deploy.
Take a minute to read the test suite in `tests/test_Account.py`.
It's deployed like any other contract in a test, while also passing a public key parameter to the constructor:
```python=
@pytest.fixture(scope='module')
async def account_factory():
starknet = await Starknet.empty()
account = await starknet.deploy(
"contracts/Account.cairo",
constructor_calldata=[signer.public_key]
)
return starknet, account
```
Calling a contract function is done using:
1) a `Signer` helper that wraps your key-pair
2) a `StarknetContract` to send the transaction to, after going through the account contract
3) a function name on that contract being called
4) the calldata
```python=
@pytest.mark.asyncio
async def test_execute(account_factory):
starknet, account = account_factory
initializable = await starknet.deploy("contracts/Initializable.cairo")
execution_info = await initializable.initialized().call()
assert execution_info.result == (0,)
await signer.send_transaction(account, initializable.contract_address, 'initialize', [])
execution_info = await initializable.initialized().call()
assert execution_info.result == (1,)
```
## Modifying our contract
Now, that we've included an off-the shelf account implementation, we'll want to refactor our `contracts/contract.cairo` to delegate the signature verification to it. This allows us to worry less about signature schemes and replay attacks. Instead, our code can rely on the assumption that an account contract executing the tx has already signed-off on the action.
Our storage variables will remain largely the same, though we no longer need to keep track of nonces, and instead have a `state_id`:
```python=
#
# Storage
#
# Who owns/controls the car (can update signing authority)
@storage_var
func vehicle_owner_address(vehicle_id : felt) -> (address : felt):
end
# Who signs commitments on behalf of the car
@storage_var
func vehicle_signer_address(vehicle_id : felt) -> (address : felt):
end
# Hashes for vehicle state at some id
@storage_var
func vehicle_state(vehicle_id : felt, state_id : felt) -> (state_hash : felt):
end
```
> ✨ Exercise: using the updated storage vars, update our getters to reflect the changes
*(answer hidden below)*
:::spoiler
```python=
#
# Getters
#
@view
func get_owner{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt) -> (owner_address : felt):
let (owner_address) = vehicle_owner_address.read(vehicle_id=vehicle_id)
return (owner_address=owner_address)
end
@view
func get_signer{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt) -> (signer_address : felt):
let (signer_address) = vehicle_signer_address.read(vehicle_id=vehicle_id)
return (signer_address=signer_address)
end
@view
func get_state{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, state_id : felt) -> (state_hash : felt):
let (state_hash) = vehicle_state.read(vehicle_id=vehicle_id, state_id=state_id)
return (state_hash=state_hash)
end
```
:::
---
When registering a vehicle, we can use `get_caller_address()` to implicitly determine who the vehicle owner will be:
```python=
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.cairo.common.math import assert_not_zero
from starkware.starknet.common.syscalls import get_caller_address
...
# Initializes the vehicle with a given owner & signer
@external
func register_vehicle{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, signer_address : felt):
# Verify that the vehicle ID is available
let (is_vehicle_id_taken) = vehicle_owner_address.read(vehicle_id=vehicle_id)
assert is_vehicle_id_taken = 0
# Caller is the owner. Verify caller & signer are non zero
let (owner_address) = get_caller_address()
assert_not_zero(owner_address)
assert_not_zero(signer_address)
# Initialize the vehicle's owner and signer
vehicle_owner_address.write(vehicle_id=vehicle_id, value=owner_address)
vehicle_signer_address.write(vehicle_id=vehicle_id, value=signer_address)
return ()
end
```
A user can now deploy their owner account contract, their delegated signing contract, and register them to some vehicle id.
The meat of the application used to live in the `attest_state()` function. Because we've offloaded the signature verification to the account contract, we can gut a lot of the existing code:
```python=
# Vehicle signers can attest to a state hash -- data storage & verification off-chain
@external
func attest_state{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, state_id : felt, state_hash : felt):
# TODO: Verify the vehicle has been registered & the caller is the SIGNER (not the owner)
# TODO: Make sure a unique state id was used
# Register state
vehicle_state.write(vehicle_id=vehicle_id, state_id=state_id, value=state_hash)
return ()
end
```
Leaving us with only a few restrictions to enforce on the contract calls.
> ✨ Exercise: Are you able to implement the checks marked as TODOs above?
*(answer hidden below)*
:::spoiler
```python=
# Vehicle signers can attest to a state hash -- data storage & verification off-chain
@external
func attest_state{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, state_id : felt, state_hash : felt):
# Verify the vehicle has been registered & the caller is the signer
let (signer_address) = vehicle_signer_address.read(vehicle_id=vehicle_id)
let (caller) = get_caller_address()
assert_not_zero(caller)
assert signer_address = caller
# Make sure a unique state id was used
let (state) = vehicle_state.read(vehicle_id=vehicle_id, state_id=state_id)
assert state = 0
# Register state
vehicle_state.write(vehicle_id=vehicle_id, state_id=state_id, value=state_hash)
return ()
end
```
:::
---
That brings us to the `set_signer()` function:
```python=
# Vehicle owners can change the signing authority for a car they own
@external
func set_signer{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, signer_address : felt):
# TODO: implement
return ()
end
```
> ✨ Exercise: Implement `set_signer` -- the logic will be very similar to `attest_state()`, though the vehicle owner must authorize the tx instead of the vehicle signer.
*(answer hidden below)*
:::spoiler
```python=
# Vehicle owners can change the signing authority for a car they own
@external
func set_signer{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, signer_address : felt):
# Verify the vehicle has been registered & the caller is the owner
let (owner_address) = vehicle_owner_address.read(vehicle_id=vehicle_id)
let (caller) = get_caller_address()
assert_not_zero(caller)
assert owner_address = caller
# Update signer
vehicle_signer_address.write(vehicle_id=vehicle_id, value=signer_address)
return ()
end
```
:::
## Testing our changes
We start off very similar to our existing test setup code, deploying the application contract. In addition, two account contracts will also be deployed to the local devnet: one for the vehicle owner and signer.
```python=
from dataclasses import dataclass
from typing import Tuple
import pytest
import asyncio
from starkware.starknet.testing.starknet import Starknet, StarknetContract
from starkware.starkware_utils.error_handling import StarkException
from utils import Signer
some_vehicle = 1
@dataclass
class Account:
signer: Signer
contract: StarknetContract
@pytest.fixture(scope="module")
def event_loop():
return asyncio.new_event_loop()
# Reusable local network & contracts to save testing time
@pytest.fixture(scope="module")
async def contract_factory() -> Tuple[Starknet, Account, Account, StarknetContract]:
starknet = await Starknet.empty()
some_signer = Signer(private_key=12345)
owner_account = Account(
signer=some_signer,
contract=await starknet.deploy(
"contracts/Account.cairo",
constructor_calldata=[some_signer.public_key]
)
)
some_other_signer = Signer(private_key=123456789)
signer_account = Account(
signer=some_other_signer,
contract=await starknet.deploy(
"contracts/Account.cairo",
constructor_calldata=[some_other_signer.public_key]
)
)
contract = await starknet.deploy("contracts/contract.cairo")
return starknet, owner_account, signer_account, contract
```
Using the `utils.Signer` that wraps our key-pair, we'll sign a transaction that targets the `register_vehicle` function with the right calldata. This transaction will go through the account contract, and upon successful signature verification, be executed against our dapp's contract.
```python=
@pytest.mark.asyncio
async def test_register_vehicle(contract_factory):
"""Should register a vehicle to a given owner address"""
_, owner_account, signer_account, contract = contract_factory
await owner_account.signer.send_transaction(
account=owner_account.contract,
to=contract.contract_address,
selector_name='register_vehicle',
calldata=[some_vehicle, signer_account.contract.contract_address],
)
# Check the owner is registered
observed_registrant = await contract.get_owner(vehicle_id=some_vehicle).call()
assert observed_registrant.result == (owner_account.contract.contract_address,)
# ... and the signer
observed_signer = await contract.get_signer(vehicle_id=some_vehicle).call()
assert observed_signer.result == (signer_account.contract.contract_address,)
```
That covers the happy path of registration. What happens when a vehicle is re-registered?
> ✨ Exercise: Implement a test that asserts a vehicle can only be registered once
*(answer hidden below)*
:::spoiler
```python=
@pytest.mark.asyncio
async def test_register_vehicle_again(contract_factory):
"""Should fail to register a vehicle a second time"""
_, owner_account, signer_account, contract = contract_factory
with pytest.raises(StarkException):
await signer_account.signer.send_transaction(
account=signer_account.contract,
to=contract.contract_address,
selector_name="register_vehicle",
calldata=[some_vehicle, signer_account.contract.contract_address],
)
# Check the original owner is still registered
observed_registrant = await contract.get_owner(vehicle_id=some_vehicle).call()
assert observed_registrant.result == (owner_account.contract.contract_address,)
```
:::
---
As for committing a state hash, we can reuse a lot of the code we just wrote. The happy path consists of the signer account (not the owner) submitting a new hash for its vehicle, followed by successfully reading that value back out of the contract.
```python=
@pytest.mark.asyncio
async def test_attest_state(contract_factory):
"""Should successfully attest to a state hash"""
_, _, signer_account, contract = contract_factory
state_id = 1
state_hash = 1234
await signer_account.signer.send_transaction(
account=signer_account.contract,
to=contract.contract_address,
selector_name="attest_state",
calldata=[some_vehicle, state_id, state_hash],
)
# Check the state hash was committed
observed_state = await contract.get_state(
vehicle_id=some_vehicle, state_id=state_id
).call()
assert observed_state.result == (state_hash,)
```
Can we call `attest_state()` on an unregisterd vehicle?
```python=
@pytest.mark.asyncio
async def test_attest_state_unregistered_vehicle(contract_factory):
"""Should fail with an unregistered vehicle"""
_, _, signer_account, contract = contract_factory
state_id = 1
state_hash = 1234
some_unregistered_vehicle = 5
with pytest.raises(StarkException):
await signer_account.signer.send_transaction(
signer_account.contract,
contract.contract_address,
"attest_state",
[some_unregistered_vehicle, state_id, state_hash],
)
```
> ✨ Exercise: Any other edge cases we might want to test for?
*(potential answers hidden below)*
:::spoiler
Signing an attestation with an account other than the authorized signer:
```python=
@pytest.mark.asyncio
async def test_attest_state_invalid_account(contract_factory):
"""Should fail when attesting from owner instead of signer"""
_, owner_account, _, contract = contract_factory
state_id = 1
state_hash = 1234
with pytest.raises(StarkException):
# Attest with owner rather than delegate signer
await owner_account.signer.send_transaction(
account=owner_account.contract,
to=contract.contract_address,
selector_name="attest_state",
calldata=[some_vehicle, state_id, state_hash],
)
```
Or calling the function with no account at all:
```python=
@pytest.mark.asyncio
async def test_attest_state_no_account(contract_factory):
"""Should fail to commit state if no account signed the tx"""
_, _, _, contract = contract_factory
with pytest.raises(StarkException):
# Transaction not sent through an account
await contract.attest_state(
vehicle_id=some_vehicle,
state_id=5,
state_hash=4567,
).invoke()
```
:::
---
Testing the `set_signer` function is practically the same thing, only minor tweaks. Feel free to try and implement some coverage for these functions, or just peek at the [part3's tests](https://github.com/sambarnes/fullstack-starknet/blob/master/part3/starknet/tests/test_contract.py#L156-L201) in the github repo.
## To be continued...
Our python application is still using our custom signature scheme. In the next part of the series, we'll need to modify it to send transactions through a deployed account contract.