# Full-Stack Starknet
* **[Part 1]** π§ [Getting Started in Cairo & Deploying with Nile](https://hackmd.io/@sambarnes/BJvGs0JpK) (***you are here***)
* **[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)
* **[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). Corrections/suggestions welcome :)*
> **NOTE**: *Since this space moves so quickly, a lot of the libraries used are shifting underneath this tutorial.*
> *If you see something that isn't working, you are probably using the newest version of a contract or library. Tweaks are likely necessary!*
> **NOTENOTE**: These tutorials were written using Nile. Since then, however, a foundry-esque testing framework called [protostar](https://docs.swmansion.com/protostar/) has been released. I would urge most people to use this, as the benefits of testing in the same language as your contracts is not to be understated.
In this series, we'll create a primitive smart contract, learning all of the building blocks you'll need to get started writing your own real world dapps on StarkNet. The application will be a "blackbox" for cars. Similar to those found on airplanes, recording flight statistics and diagnostics, we'll make one for standard automobiles.
Most modern vehicles have an [OBD2 port](https://en.wikipedia.org/wiki/On-board_diagnostics) where owners or mechanics can read information about the vehicle -- anything from mileage, to speed, to throttle position. A raspberry pi will read this data, along with dashcam footage, and periodically post hashes of the recent segments on StarkNet. The actual diagnostics data will be stored offline, but able to be rehashed & verified later if needed.
If the owner is in a car accident or pulled over, they will have timestamped proof of their data on chain, providing more detail to the process of ["traffic collision reconstruction"](https://en.wikipedia.org/wiki/Traffic_collision_reconstruction) or disputing a traffic violation. Sure, it's possible for this data to be spoofed between the time its recorded and committed on chain. Though, it's hard to know what exactly you'll need to lie about in the future, and -- with a sufficiently small interval -- it would be unlikely that a coherent lie could be spoofed, posted on chain, and make sense within the broader context of the incident.
Plus, this is just an example :)
> β WARNING: All code used & linked to in this tutorial is experimental and not ready for production use. StartNet is new, evolving, and will inevitably see contract exploits similar to the original DAO re-entrancy bug.
>
> Feel free to reach out with comments, suggestions, or corrections
## Design
![Design diagram](https://i.imgur.com/i0ZFjfO.png)
> NOTE: Raspberry pi & hardware not actually needed. Feel free to do so if you want!
We'll start with two general components, in their own folders for cleaner separation of concerns:
```
sam@sam:~/fullstack-starknet/part1$ tree -L 1
.
βββ pi # the application running on a raspberry pi
βββ starknet # the contracts deployed on starknet
```
Let's build the application deployed on StarkNet, since thats just more fun :)
In later parts of this series, we'll build out the rest more fully.
## StarkNet Project Setup
First, we'll need to [initialize project with Nile](https://medium.com/@martriay/manage-your-starknet-deployments-with-nile-%EF%B8%8F-e849d40546dd). Using the python module `venv`, create a workspace for your cairo code and install the Nile project manager:
```
sam@sam:~/fullstack-starknet/part1/starknet$ python3 -m venv env
sam@sam:~/fullstack-starknet/part1/starknet$ source env/bin/activate
(env) sam@sam:~/fullstack-starknet/part1/starknet$ python3 -m pip install cairo-nile
```
A simple `nile init` will bootstrap the project's structure:
```
(env) sam@sam:~/fullstack-starknet/part1/starknet$ nile init
...
β Dependencies successfully installed
π Creating project directory tree
β΅οΈ Nile project ready! Try running:
nile compile
```
Inspecting the generated file hierarchy, we now have:
```
(env) sam@sam:~/fullstack-starknet/part1/starknet$ tree
.
βββ accounts.json
βββ contracts
βΒ Β βββ contract.cairo # A "hello world" contract with basic storage, getters, and setters
βββ Makefile
βββ tests
βββ test_contract.py
3 directories, 5 files
```
Our last command reccommended running `nile compile`, this can also be done with the makefile:
```
(env) sam@sam:~/fullstack-starknet/part1/starknet$ make build
nile compile
π€ Compiling all Cairo contracts in the contracts directory
π¨ Compiling contracts/contract.cairo
β Done
```
The [unit testing setup](https://perama-v.github.io/cairo/pytest) can also be run similarly:
```
(env) sam@sam:~/fullstack-starknet/part1/starknet$ make test
pytest tests/
====================================== test session starts ======================================
platform linux -- Python 3.7.12, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /home/sam/fullstack-starknet/part1/starknet
plugins: typeguard-2.13.3, asyncio-0.17.0, web3-5.26.0
collected 1 item
tests/test_contract.py . [100%]
====================================== 1 passed, 6 warnings in 5.19s ======================================
```
If you haven't already, check out the contract code initialized by Nile. It's a simple counter with storage, an external modifier, and a view. An in depth explanation can be found [here in the official docs](https://www.cairo-lang.org/docs/hello_starknet/intro.html#your-first-contract). Once that's starting to make sense, this upcoming section won't be too big of a leap.
If you ever find yourself needing a refresher on syntax or other core concepts, [Perama's blog](https://perama-v.github.io/cairo/by-example/) is turning into an invaluable resource on Cairo.
## A First Look at Cairo
### Directives & Imports
All contracts will start with a section like this.
```python
# The "%lang" directive declares this code as a StarkNet contract.
%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.cairo.common.hash import hash2
from starkware.cairo.common.math import assert_not_zero
from starkware.cairo.common.signature import verify_ecdsa_signature
from starkware.starknet.common.syscalls import get_tx_signature
```
Coming from other programming languages, this should all be pretty familiar.
### Storage
Now we'll need a place to store our data, specifically:
* what public key owns a given car
* what public key the owner has granted signing authority
* hashes of the vehicle's diagnotic data
The first two are super straight forward -- simple mappings from a vehicle ID to public key.
```python=
#
# Storage
#
# Who owns/controls the car (can update signing authority)
@storage_var
func vehicle_owner_public_key(vehicle_id : felt) -> (public_key : felt):
end
# Who signs commitments on behalf of the car
@storage_var
func vehicle_signer_public_key(vehicle_id : felt) -> (public_key : felt):
end
```
The third however is a little more involved. While we could do another simple mapping from vehicle ID to state hash, this would only allow us to store one hash on chain and overwrite it.
Instead, we'll need to add a unique "nonce" to be used as an index for transactions performed on the vehicle. Similar to Ethereum transaction nonces, these will start at 0 and increment with every signed operation made for the vehicle.
```python=
# Hashes for vehicle state at nonce
@storage_var
func vehicle_state(vehicle_id : felt, nonce : felt) -> (state_hash : felt):
end
```
Most importantly, a nonce also helps protect against [replay attacks](https://en.wikipedia.org/wiki/Replay_attack) -- where a malicious observer can resubmit a signed transaction and modify the state without the owner/signer's keys. By requiring that a new & unique nonce be included in the signed payload, we can be sure that a signature produced now and published on chain will not be able to be useful to attackers in the future.
In order to verify our transactions later, we'll need a way to keep track of the current expected nonce.
> β¨ Exercise: try to implement another storage variable `vehicle_nonce()` that maps from `vehicle_id` to `nonce`
*(answer hidden below)*
:::spoiler
```python=
# Stores the nonce expected for the next transaction (including key management & state commitments)
@storage_var
func vehicle_nonce(vehicle_id : felt) -> (nonce : felt):
end
```
Easy enough?
:::
### Getters
We've got a place to store data, now we'll need some functions to access it. Our getters will be annotated with `@view` -- meaning that the method only queries the state without modifying it.
> β WARNING: In the current version, this is NOT enforced by the compiler. Be sure to check that @view functions don't modify state.
```python=
#
# Getters
#
@view
func get_owner{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt) -> (public_key : felt):
let (public_key) = vehicle_owner_public_key.read(vehicle_id=vehicle_id)
return (public_key=public_key)
end
@view
func get_signer{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt) -> (public_key : felt):
let (public_key) = vehicle_signer_public_key.read(vehicle_id=vehicle_id)
return (public_key=public_key)
end
@view
func get_state{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, nonce: felt) -> (state_hash : felt):
let (state_hash) = vehicle_state.read(vehicle_id=vehicle_id, nonce=nonce)
return (state_hash=state_hash)
end
```
In each of the above methods, we take in a vehicle ID, read directly from storage, and return the value found.
The implicit arguments in `{ curly braces }` might feel a little strange. Another excerpt from the [official documentation](https://www.cairo-lang.org/docs/hello_starknet/intro.html#your-first-contract) explains them in a bit more detail:
> Consider the implicit arguments: `syscall_ptr`, `pedersen_ptr` and `range_check_ptr`:
> * You should be familiar with `pedersen_ptr`, which allows to compute the Pedersen hash function, and `range_check_ptr`, which allows to compare integers. But it seems that the contract doesnβt use any hash function or integer comparison, so why are they needed? The reason is that storage variables require these implicit arguments in order to compute the actual memory address of this variable. This may not be needed in simple variables, but with maps (see [Storage maps](https://www.cairo-lang.org/docs/hello_starknet/user_auth.html#storage-maps)) computing the Pedersen hash is part of what `read()` and `write()` do.
> * `syscall_ptr` is a new primitive, unique to StarkNet contracts (it doesnβt exist in Cairo). `syscall_ptr` allows the code to invoke system calls. It is also implicit arguments of `read()` and `write()` (this time, because storage access is done using system calls).
They may still feel weird and annoying, but without them, these variables would need to be passed around everywhere.
Before we move onto modifying contract state, take a shot at implementing a function to fetch a vehicle's current nonce. We'll need to query this in the next section.
> β¨ Exercise: try to implement another view `get_nonce()` to read & return the `nonce` for a given `vehicle_id`
*(answer hidden below)*
:::spoiler
Something like this?
```python=
@view
func get_nonce{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt) -> (nonce : felt):
let (nonce) = vehicle_nonce.read(vehicle_id=vehicle_id)
return (nonce=nonce)
end
```
Remember the implicit arguments (i.e. `syscall_ptr, pedersen_ptr, range_check_ptr`) are required for functions reading from or writing to storage.
:::
### Setters
Time to tie it all together with `@external` functions that modify contract state. We're going to need the following actions:
* `register_vehicle` -- assign a vehicle ID to an owner and signer (public keys, potentially the same one)
* `attest_state` -- a function for a vehicle's authorized signer (our raspberry pi) to make commitments about the car's state
* `set_signer` -- a management function allowing the vehicle owner to swap out the authorized signer
#### Vehicle Registration
Starting with vehicle registration, we'll make a function where anyone can register a car to a owner's public key.
For now, we'll keep this function unsigned. Can you think of scenarios where this would be desirable? Where it could be harmful? Perhaps a government entity would want only one authorized registration key?
```python=
#
# Setters
#
# Initializes the vehicle with a given owner & signer
@external
func register_vehicle{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}(
vehicle_id : felt, owner_public_key : felt, signer_public_key : felt):
# Verify that the vehicle ID is available
let (is_vehicle_id_taken) = vehicle_owner_public_key.read(vehicle_id=vehicle_id)
assert is_vehicle_id_taken = 0
# In cairo, everything uninitialized will by default have a zero value. It is
# important to keep this in mind when designing your contracts, as it will be
# difficult to distinguish between a variable explicitly set to zero and one
# that simply hasn't been initialized yet.
# Initialize the vehicle's owner and signer
vehicle_owner_public_key.write(vehicle_id=vehicle_id, value=owner_public_key)
vehicle_signer_public_key.write(vehicle_id=vehicle_id, value=signer_public_key)
# Because this function isn't signed, there is no need to increment the vehicle nonce.
return ()
end
```
To test our functionality so far, let's modify the unit test suite at `tests/test_contract.py`:
```python=
import pytest
import asyncio
from starkware.crypto.signature.signature import (
pedersen_hash,
private_to_stark_key,
sign,
)
from starkware.starknet.testing.starknet import Starknet
from starkware.starkware_utils.error_handling import StarkException
@pytest.fixture(scope="module")
def event_loop():
return asyncio.new_event_loop()
# Reusable local network & contract to save testing time
@pytest.fixture(scope="module")
async def contract_factory():
starknet = await Starknet.empty()
contract = await starknet.deploy("contracts/contract.cairo")
return starknet, contract
# Some mock keypairs to test with
some_vehicle = 1
some_owner_secret = 12345
some_owner = private_to_stark_key(some_owner_secret)
some_signer_secret = 123456789
some_signer = private_to_stark_key(some_signer_secret)
some_other_signer_secret = 9876754321
some_other_signer = private_to_stark_key(some_other_signer_secret)
# The testing library uses python's asyncio. So the following
# decorator and the ``async`` keyword are needed.
@pytest.mark.asyncio
async def test_register_vehicle(contract_factory):
"""Should register a vehicle to a given public key"""
_, contract = contract_factory
await contract.register_vehicle(
vehicle_id=some_vehicle,
owner_public_key=some_owner,
signer_public_key=some_signer,
).invoke()
# Check the owner is registered
registrant = await contract.get_owner(vehicle_id=some_vehicle).call()
assert registrant.result == (some_owner,)
# ... and the signer
signer = await contract.get_signer(vehicle_id=some_vehicle).call()
assert signer.result == (some_signer,)
```
Running `make test` should kick off a pytest session & voila β¨
```
=============================== test session starts ===============================
platform linux -- Python 3.7.12, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /home/sam/fullstack-starknet/part1/starknet
plugins: asyncio-0.17.1, typeguard-2.13.3, web3-5.26.0
asyncio: mode=legacy
collected 1 items / 0 deselected / 1 selected
tests/test_contract.py . [100%]
================== 1 passed, 0 deselected, 0 warnings in 15.75s ===================
```
#### State Commitments (Transaction Signatures)
Now that a vehicle can be registered, we also need a way for authorized signers to make commitments about the current vehicle state. For this, we'll need a function that takes in a vehicle ID, a nonce, and a hash of the current vehicle state.
We'll also need to ensure this transaction is signed, so all TXs with invalid signatures will be reverted & unable to modify contract state. From the [official docs on transaction signing](https://www.cairo-lang.org/docs/hello_starknet/user_auth.html):
> While we could add the signature to the transaction calldata (that is, add it as additional arguments to the smart contract function) [as seen in the [StarkNet voting workshop](https://www.youtube.com/watch?v=fpwSdNnzulM)], StarkNet has a special mechanism for handling transaction signatures, freeing the developer from including them in the transaction calldata. This does not mean that you must use a specific signature scheme, just that the signature data may be kept separately from the calldata. The system call function `get_tx_signature()` returns the length and data of the signature supplied with the transaction. It is up to the contract author to check that the signature is valid. Note that this function requires the `syscall_ptr` implicit argument.
Because ECDSA signatures work efficiently with STARKs and are natively supported on StarkNet, we'll use those.
```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,
ecdsa_ptr : SignatureBuiltin*}(vehicle_id : felt, nonce : felt, state_hash : felt):
# Note the addition of an ecdsa_ptr implicit argument, this is required in functions
# that verify ECDSA signatures.
# Verify the vehicle has been registered with a signer
let (signer_public_key) = vehicle_signer_public_key.read(vehicle_id=vehicle_id)
assert_not_zero(signer_public_key)
# Make sure the current nonce was used
let (expected_nonce) = vehicle_nonce.read(vehicle_id=vehicle_id)
assert expected_nonce - nonce = 0
# Expected Signed Message = H( nonce + H( vehicle_id , H( signer_public_key ) ) )
let (h1) = hash2{hash_ptr=pedersen_ptr}(state_hash, 0)
let (h2) = hash2{hash_ptr=pedersen_ptr}(vehicle_id, h1)
let (message_hash) = hash2{hash_ptr=pedersen_ptr}(nonce, h2)
# Verify signature is valid and covers the expected signed message
let (sig_len : felt, sig : felt*) = get_tx_signature()
assert sig_len = 2 # ECDSA signatures have two parts, r and s
verify_ecdsa_signature(
message=message_hash, public_key=signer_public_key, signature_r=sig[0], signature_s=sig[1])
# If the contract passes ^this line, the signaure verification passed.
# Otherwise, the execution would halt and the transaction would revert.
# Register state & increment nonce
vehicle_state.write(vehicle_id=vehicle_id, nonce=nonce, value=state_hash)
vehicle_nonce.write(vehicle_id=vehicle_id, value=nonce + 1)
return ()
end
```
Building on our unit suite, lets test some invalid cases as well as the happy path:
* TXs signed with an invalid nonce should fail
* TXs with invalid signatures should fail
* with everything correct & verified, the TX should succeed
```python=
@pytest.mark.asyncio
async def test_attest_state_invalid_nonce(contract_factory):
"""Should fail with invalid nonce"""
_, contract = contract_factory
state_hash = 1234
nonce = 666
message_hash = pedersen_hash(
nonce, pedersen_hash(some_vehicle, pedersen_hash(state_hash, 0))
)
sig_r, sig_s = sign(msg_hash=message_hash, priv_key=some_signer_secret)
with pytest.raises(StarkException):
await contract.attest_state(
vehicle_id=some_vehicle,
nonce=nonce,
state_hash=state_hash,
).invoke(signature=[sig_r, sig_s])
@pytest.mark.asyncio
async def test_attest_state_invalid_signature(contract_factory):
"""Should fail with invalid nonce"""
_, contract = contract_factory
with pytest.raises(StarkException):
await contract.attest_state(
vehicle_id=some_vehicle,
nonce=0,
state_hash=1234,
).invoke(signature=[123456789, 987654321])
@pytest.mark.asyncio
async def test_attest_state(contract_factory):
"""Should successfully attest to a state hash & increment nonce"""
_, contract = contract_factory
state_hash = 1234
nonce = 0
message_hash = pedersen_hash(
nonce, pedersen_hash(some_vehicle, pedersen_hash(state_hash, 0))
)
sig_r, sig_s = sign(msg_hash=message_hash, priv_key=some_signer_secret)
await contract.attest_state(
vehicle_id=some_vehicle,
nonce=nonce,
state_hash=state_hash,
).invoke(signature=[sig_r, sig_s])
# Check the nonce was incremented
new_nonce = await contract.get_nonce(vehicle_id=some_vehicle).call()
assert new_nonce.result == (nonce + 1,)
```
There's one last case that our function could fail for. Can you spot it?
*(anwer hidden)*
:::spoiler
A commitment sent for an unregistered vehicle!
:::
---
> β¨ Exercise: Implement one last test to cover this edge case
*(anwer hidden)*
:::spoiler
```python=
@pytest.mark.asyncio
async def test_attest_state_unregistered_vehicle(contract_factory):
"""Should fail with an unregistered vehicle"""
_, contract = contract_factory
state_hash = 1234
nonce = 0
some_unregistered_vehicle = 5
message_hash = pedersen_hash(
nonce, pedersen_hash(some_vehicle, pedersen_hash(state_hash, 0))
)
sig_r, sig_s = sign(msg_hash=message_hash, priv_key=some_signer_secret)
with pytest.raises(StarkException):
await contract.attest_state(
vehicle_id=some_unregistered_vehicle,
nonce=nonce,
state_hash=state_hash,
).invoke(signature=[sig_r, sig_s])
```
:::
#### Signing Authority Management
What happens if the vehicle owner's raspberry pi breaks or the signer key is compromised? We'll need to implement a `set_signer()` function to allow vehicle owners to update the signer public key
> β¨ Exercise: using the `attest_state` function as a guide, write the functionality that let's an owner swap out the current 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,
ecdsa_ptr : SignatureBuiltin*}(vehicle_id : felt, nonce : felt, signer_public_key : felt):
# Verify the vehicle has been registered with an owner
let (owner_public_key) = vehicle_owner_public_key.read(vehicle_id=vehicle_id)
assert_not_zero(owner_public_key)
# Make sure the current nonce was used
let (expected_nonce) = vehicle_nonce.read(vehicle_id=vehicle_id)
assert expected_nonce - nonce = 0
# Verify signature
# Signed Message = H( nonce + H( vehicle_id , H( signer_public_key ) ) )
let (h1) = hash2{hash_ptr=pedersen_ptr}(signer_public_key, 0)
let (h2) = hash2{hash_ptr=pedersen_ptr}(vehicle_id, h1)
let (message_hash) = hash2{hash_ptr=pedersen_ptr}(nonce, h2)
let (sig_len : felt, sig : felt*) = get_tx_signature()
assert sig_len = 2
verify_ecdsa_signature(
message=message_hash, public_key=owner_public_key, signature_r=sig[0], signature_s=sig[1])
# Update signer & increment nonce
vehicle_signer_public_key.write(vehicle_id=vehicle_id, value=signer_public_key)
vehicle_nonce.write(vehicle_id=vehicle_id, value=nonce + 1)
return ()
end
```
:::
---
Are you able to create some test cases for its behavior?
*(answer hidden below)*
:::spoiler
```python=
@pytest.mark.asyncio
async def test_set_signer_invalid_nonce(contract_factory):
"""Should fail to update the signer with a bad nonce"""
_, contract = contract_factory
nonce = 666
message_hash = pedersen_hash(
nonce, pedersen_hash(some_vehicle, pedersen_hash(some_other_signer, 0))
)
sig_r, sig_s = sign(msg_hash=message_hash, priv_key=some_owner_secret)
with pytest.raises(StarkException):
await contract.set_signer(
vehicle_id=some_vehicle,
nonce=nonce,
signer_public_key=some_other_signer,
).invoke(signature=[sig_r, sig_s])
@pytest.mark.asyncio
async def test_set_signer_not_owner(contract_factory):
"""Should fail to update the signer if owner didn't sign the message"""
_, contract = contract_factory
nonce = await contract.get_nonce(vehicle_id=some_vehicle).call()
nonce = nonce.result[0]
message_hash = pedersen_hash(
nonce, pedersen_hash(some_vehicle, pedersen_hash(some_other_signer, 0))
)
# Error here: signing with vehicle signer, not owner
sig_r, sig_s = sign(msg_hash=message_hash, priv_key=some_signer_secret)
with pytest.raises(StarkException):
await contract.set_signer(
vehicle_id=some_vehicle,
nonce=nonce,
signer_public_key=some_other_signer,
).invoke(signature=[sig_r, sig_s])
@pytest.mark.asyncio
async def test_set_signer(contract_factory):
"""Should successfully update the signer for the car"""
_, contract = contract_factory
nonce = await contract.get_nonce(vehicle_id=some_vehicle).call()
nonce = nonce.result[0]
message_hash = pedersen_hash(
nonce, pedersen_hash(some_vehicle, pedersen_hash(some_other_signer, 0))
)
sig_r, sig_s = sign(msg_hash=message_hash, priv_key=some_owner_secret)
await contract.set_signer(
vehicle_id=some_vehicle,
nonce=nonce,
signer_public_key=some_other_signer,
).invoke(signature=[sig_r, sig_s])
# Check that the signer is updated
new_signer = await contract.get_signer(vehicle_id=some_vehicle).call()
assert new_signer.result == (some_other_signer,)
# ... and the nonce was incremented
new_nonce = await contract.get_nonce(vehicle_id=some_vehicle).call()
assert new_nonce.result == (nonce + 1,)
```
:::
## Testnet Deployment
Fortunately, nile also provides some nice utilities for deploying our contracts too:
```
(env) sam@sam:~/fullstack-starknet/part1/starknet$ nile deploy contract --alias blackbox --network=goerli
π Deploying contract
β³ οΈDeployment of contract successfully sent at 0x029af160331cb2c5898999034f51f3357243a36b93c7b696f7daf0711482458e
π§Ύ Transaction hash: 0x2fe91a7e15d1aa9890f7feabbae25864252a4a6fd0ef042e35250f71354073a
π¦ Registering deployment as blackbox in goerli.deployments.txt
```
In the example above, we deploy `artifacts/contracts.cairo` to the goerli testnet and alias that deployment to `blackbox` so that nile can interact with it later.
Wait 5 minutes or so for the next testnet block to be produced (UX around pending transactions still being developed) and [find your transaction in the explorer](https://goerli.voyager.online/tx/0x2fe91a7e15d1aa9890f7feabbae25864252a4a6fd0ef042e35250f71354073a).
The [deployed contract](https://goerli.voyager.online/contract/0x029af160331cb2c5898999034f51f3357243a36b93c7b696f7daf0711482458e)'s page also provides a basic GUI out-of-the-box, similar to Etherscan.
Here, our `@views` into the contract state: [#readContract](https://goerli.voyager.online/contract/0x029af160331cb2c5898999034f51f3357243a36b93c7b696f7daf0711482458e#readContract)
... and our `@external` setters: [#writeContract](https://goerli.voyager.online/contract/0x029af160331cb2c5898999034f51f3357243a36b93c7b696f7daf0711482458e#writeContract)
Congrats! You've now written & deployed a contract with storage, getters/setters, & a basic access control scheme! ππ
While you could [interact with your contract](https://github.com/OpenZeppelin/nile#call-and-invoke) from the cli now, it may be better to wait for the [next part in the series](https://hackmd.io/@sambarnes/H1Fx7OMaF), where we call the contract from our python code elsewhere in the application stack. This will make signed transactions easier to create and submit.