Full-Stack Starknet:
Completed code on GitHub here.
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 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*).
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. 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.
Because it's not (yet) a native functionality of the platform, we'll copy the OpenZeppelin example implementation for now. The 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:
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:
Calling a contract function is done using:
Signer
helper that wraps your key-pairStarknetContract
to send the transaction to, after going through the account contractNow, 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
:
✨ Exercise: using the updated storage vars, update our getters to reflect the changes
(answer hidden below)
When registering a vehicle, we can use get_caller_address()
to implicitly determine who the vehicle owner will be:
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:
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)
That brings us to the set_signer()
function:
✨ Exercise: Implement
set_signer
– the logic will be very similar toattest_state()
, though the vehicle owner must authorize the tx instead of the vehicle signer.
(answer hidden below)
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.
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.
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)
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.
Can we call attest_state()
on an unregisterd vehicle?
✨ Exercise: Any other edge cases we might want to test for?
(potential answers hidden below)
Signing an attestation with an account other than the authorized signer:
Or calling the function with no account at all:
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 in the github repo.
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.