Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Nile's Signer #283

Merged
merged 16 commits into from
May 6, 2022
67 changes: 54 additions & 13 deletions docs/Account.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Accounts

Unlike Ethereum where accounts are directly derived from a private key, there's no native account concept on StarkNet.

Instead, signature validation has to be done at the contract level. To relieve smart contract applications such as ERC20 tokens or exchanges from this responsibility, we make use of Account contracts to deal with transaction authentication.
Expand All @@ -10,7 +11,8 @@ A more detailed writeup on the topic can be found on [Perama's blogpost](https:/
* [Quickstart](#quickstart)
* [Standard Interface](#standard-interface)
* [Keys, signatures and signers](#keys-signatures-and-signers)
* [Signer utility](#signer-utility)
* [Signer](#signer)
* [ActivatedSigner utility](#activatedsigner-utility)
* [Call and MultiCall format](#call-and-multicall-format)
* [API Specification](#api-specification)
* [`get_public_key`](#get_public_key)
Expand All @@ -26,14 +28,15 @@ A more detailed writeup on the topic can be found on [Perama's blogpost](https:/
## Quickstart

The general workflow is:

1. Account contract is deployed to StarkNet
2. Signed transactions can now be sent to the Account contract which validates and executes them

In Python, this would look as follows:

```cairo
```python
from starkware.starknet.testing.starknet import Starknet
signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)
starknet = await Starknet.empty()

# 1. Deploy Account
Expand Down Expand Up @@ -88,23 +91,58 @@ While the interface is agnostic of signature validation schemes, this implementa

Note that although the current implementation works only with StarkKeys, support for Ethereum's ECDSA algorithm will be added in the future.

### Signer utility
### Signer

The signer is responsible for creating a transaction signature with the user's private key for a given transaction. This implementation utilizes [Nile's Signer](https://github.com/OpenZeppelin/nile/blob/main/src/nile/signer.py) class to create transaction signatures through the `Signer` method `sign_transaction`.

`sign_transaction` expects the following parameters per transaction:

* `sender` the contract address invoking the tx
* `calls` a list containing a sublist of each call to be sent. Each sublist must consist of:
1. `to` the address of the target contract of the message
2. `selector` the function to be called on the target contract
3. `calldata` the parameters for the given `selector`
* `nonce` an unique identifier of this message to prevent transaction replays. Current implementation requires nonces to be incremental
* `max_fee` the maximum fee a user will pay

Which returns:

* `calls` a list of calls to be bundled in the transaction
* `calldata` a list of arguments for each call
* `sig_r` the transaction signature
* `sig_s` the transaction signature

While the `Signer` class performs much of the work for a transaction to be sent, it neither manages nonces nor invokes the actual transaction on the Account contract. Those functions can be done manually; however, this implementation abstracts that all away with `ActivatedSigner`.
andrew-fleming marked this conversation as resolved.
Show resolved Hide resolved

The `Signer()` class in [utils.py](../tests/utils.py) is used to perform transactions on a given Account, crafting the tx and managing nonces.
### ActivatedSigner utility

It exposes three functions:
The `ActivatedSigner` class in [utils.py](../tests/utils.py) is used to perform transactions on a given Account, crafting the tx and managing nonces. In order for a transaction to be sent, this utility performs the following:
andrew-fleming marked this conversation as resolved.
Show resolved Hide resolved

* `def sign(message_hash)` receives a hash and returns a signed message of it
* `def send_transaction(account, to, selector_name, calldata, nonce=None, max_fee=0)` returns a future of a signed transaction, ready to be sent.
* `def send_transactions(account, calls, nonce=None, max_fee=0)` returns a future of batched signed transactions, ready to be sent.
* checks nonce
* if none is given, it fetches the nonce from the Account contract via `get_nonce`

To use Signer, pass a private key when instantiating the class:
* reformats callarray
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should "callarray" be formatted somehow? has been defined before? reformat how?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions. It's really just converting callarray[0] to hex so "reformat" isn't correct. I'll better explain this and include where it's first defined as well.

* a necessary process to convert the `to` contract address to hexadecimal format
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Nile converts to to base-16 which requires the to address to be a string. Converting the address with str() creates an integer that exceeds Cairo's FIELD_PRIME...I'll add this to the docs :)


* passes transaction data to Nile's `Signer`
* this returns the signature for the transaction as well as the `calls` and `calldata`
andrew-fleming marked this conversation as resolved.
Show resolved Hide resolved

* invokes the Account contract's `__execute__` method
* where the transaction is finally sent
andrew-fleming marked this conversation as resolved.
Show resolved Hide resolved

Users only need to interact with the following exposed methods to perform a transaction:

* `send_transaction(account, to, selector_name, calldata, nonce=None, max_fee=0)` returns a future of a signed transaction, ready to be sent.

* `send_transactions(account, calls, nonce=None, max_fee=0)` returns a future of batched signed transactions, ready to be sent.

To use `ActivatedSigner`, pass a private key when instantiating the class:

```python
from utils import Signer
from utils import ActivatedSigner

PRIVATE_KEY = 123456789987654321
signer = Signer(PRIVATE_KEY)
signer = ActivatedSigner(PRIVATE_KEY)
```

Then send single transactions with the `send_transaction` method.
Expand Down Expand Up @@ -170,6 +208,7 @@ Where:
* `version` is a fixed number which is used to invalidate old transactions

This `MultiCall` message is built within the `__execute__` method which has the following interface:

```cairo
func __execute__(
call_array_len: felt,
Expand All @@ -195,7 +234,7 @@ Where:
await signer.send_transaction(account, account.contract_address, 'set_public_key', [NEW_KEY])
```

Note that Signer's `send_transaction` and `send_transactions` call `__execute__` under the hood.
Note that `ActivatedSigner`'s `send_transaction` and `send_transactions` call `__execute__` under the hood.

Or if you want to update the Account's L1 address on the `AccountRegistry` contract, you would

Expand All @@ -206,6 +245,7 @@ await signer.send_transaction(account, registry.contract_address, 'set_L1_addres
You can read more about how messages are structured and hashed in the [Account message scheme discussion](https://github.com/OpenZeppelin/cairo-contracts/discussions/24). For more information on the design choices and implementation of multicall, you can read the [How should Account multicall work discussion](https://github.com/OpenZeppelin/cairo-contracts/discussions/27).

> Note that the scheme of building multicall transactions within the `__execute__` method will change once StarkNet allows for pointers in struct arrays. In which case, multiple transactions can be passed to (as opposed to built within) `__execute__`.

## API Specification

This in a nutshell is the Account contract public API:
Expand Down Expand Up @@ -338,6 +378,7 @@ Currently, there's only a single library/preset Account scheme, but we're lookin
## L1 escape hatch mechanism

*[unknown, to be defined]*

## Paying for gas

*[unknown, to be defined]*
2 changes: 1 addition & 1 deletion docs/ERC20.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ erc20 = await starknet.deploy(
As most StarkNet contracts, it expects to be called by another contract and it identifies it through `get_caller_address` (analogous to Solidity's `this.address`). This is why we need an Account contract to interact with it. For example:

```python
signer = Signer(PRIVATE_KEY)
signer = ActivatedSigner(PRIVATE_KEY)
amount = uint(100)

account = await starknet.deploy(
Expand Down
2 changes: 1 addition & 1 deletion docs/ERC721.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ erc721 = await starknet.deploy(
To mint a non-fungible token, send a transaction like this:

```python
signer = Signer(PRIVATE_KEY)
signer = ActivatedSigner(PRIVATE_KEY)
tokenId = uint(1)

await signer.send_transaction(
Expand Down
12 changes: 5 additions & 7 deletions docs/Utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ To ease the readability of Cairo contracts, this project includes reusable [cons

## Strings

Cairo currently only provides support for short string literals (less than 32 characters). Note that short strings aren't really strings, rather, they're representations of Cairo field elements. The following methods provide a simple conversion to/from field elements.
Cairo currently only provides support for short string literals (less than 32 characters). Note that short strings aren't really strings, rather, they're representations of Cairo field elements. The following methods provide a simple conversion to/from field elements.

### `str_to_felt`

Takes an ASCII string and converts it to a field element via big endian representation.


### `felt_to_str`

Takes an integer and converts it to an ASCII string by trimming the null bytes and decoding the remaining bits.
Expand All @@ -45,7 +44,7 @@ Takes an integer and converts it to an ASCII string by trimming the null bytes a

Cairo's native data type is a field element (felt). Felts equate to 252 bits which poses a problem regarding 256-bit integer integration. To resolve the bit discrepancy, Cairo represents 256-bit integers as a struct of two 128-bit integers. Further, the low bits precede the high bits e.g.

```
```python
1 = (1, 0)
1 << 128 = (0, 1)
(1 << 128) - 1 = (340282366920938463463374607431768211455, 0)
Expand All @@ -57,7 +56,6 @@ Converts a simple integer into a uint256-ish tuple.

> Note `to_uint` should be used in favor of `uint`, as `uint` only returns the low bits of the tuple.


### `to_uint`

Converts an integer into a uint256-ish tuple.
Expand Down Expand Up @@ -185,7 +183,7 @@ assert_event_emitted(

## Memoization

Memoizing functions allow for quicker and computationally cheaper calculations which is immensely beneficial while testing smart contracts.
Memoizing functions allow for quicker and computationally cheaper calculations which is immensely beneficial while testing smart contracts.

### `get_contract_def`

Expand Down Expand Up @@ -227,6 +225,6 @@ def foo_factory(contract_defs, foo_init):
return cached_foo # return cached contracts
```

## Signer
## ActivatedSigner

`Signer` is used to perform transactions on a given Account, crafting the tx and managing nonces. See the [Account documentation](../docs/Account.md#signer-utility) for in-depth information.
`ActivatedSigner` is used to perform transactions with an instance of [Nile's Signer](https://github.com/OpenZeppelin/nile/blob/main/src/nile/signer.py) on a given Account, crafting the tx and managing nonces. The `Signer` instance creates the actual signature and `ActivatedSigner` invokes the Account contract's `__execute__` with said signature. See [ActivatedSigner utility](../docs/Account.md#activatedsigner-utility) for more information.
andrew-fleming marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions tests/access/test_Ownable.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import Signer, contract_path
from utils import ActivatedSigner, contract_path


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)


@pytest.fixture(scope='module')
Expand Down
6 changes: 3 additions & 3 deletions tests/account/test_Account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from starkware.starknet.testing.starknet import Starknet
from starkware.starkware_utils.error_handling import StarkException
from starkware.starknet.definitions.error_codes import StarknetErrorCode
from utils import Signer, assert_revert, contract_path
from utils import ActivatedSigner, assert_revert, contract_path


signer = Signer(123456789987654321)
other = Signer(987654321123456789)
signer = ActivatedSigner(123456789987654321)
other = ActivatedSigner(987654321123456789)

IACCOUNT_ID = 0xf10dbd44
TRUE = 1
Expand Down
4 changes: 2 additions & 2 deletions tests/account/test_AddressRegistry.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import Signer, contract_path
from utils import ActivatedSigner, contract_path


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)
L1_ADDRESS = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
ANOTHER_ADDRESS = 0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f

Expand Down
7 changes: 4 additions & 3 deletions tests/token/erc20/test_ERC20.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256,
TRUE, get_contract_def, cached_contract, assert_revert, assert_event_emitted, contract_path
ActivatedSigner, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256,
ZERO_ADDRESS, INVALID_UINT256, TRUE, get_contract_def, cached_contract,
assert_revert, assert_event_emitted, contract_path
)

signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# testing vars
RECIPIENT = 123
Expand Down
10 changes: 3 additions & 7 deletions tests/token/erc20/test_ERC20_Burnable_mock.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import pytest
import asyncio
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, to_uint, add_uint, sub_uint, str_to_felt, ZERO_ADDRESS, INVALID_UINT256,
get_contract_def, cached_contract, assert_revert, assert_event_emitted, contract_path
ActivatedSigner, to_uint, add_uint, sub_uint, str_to_felt, ZERO_ADDRESS, INVALID_UINT256,
get_contract_def, cached_contract, assert_revert, assert_event_emitted,
)

signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# testing vars
INIT_SUPPLY = to_uint(1000)
Expand All @@ -17,9 +16,6 @@
DECIMALS = 18


signer = Signer(123456789987654321)


@pytest.fixture(scope='module')
def contract_defs():
account_def = get_contract_def('openzeppelin/account/Account.cairo')
Expand Down
7 changes: 4 additions & 3 deletions tests/token/erc20/test_ERC20_Mintable.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, to_uint, add_uint, sub_uint, str_to_felt, MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256,
get_contract_def, cached_contract, assert_revert, assert_event_emitted
ActivatedSigner, to_uint, add_uint, sub_uint, str_to_felt,
MAX_UINT256, ZERO_ADDRESS, INVALID_UINT256, get_contract_def,
cached_contract, assert_revert, assert_event_emitted
)

signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# testing vars
RECIPIENT = 123
Expand Down
6 changes: 3 additions & 3 deletions tests/token/erc20/test_ERC20_Pausable.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, TRUE, FALSE, to_uint, str_to_felt, assert_revert, get_contract_def,
cached_contract
ActivatedSigner, TRUE, FALSE, to_uint, str_to_felt, assert_revert,
get_contract_def, cached_contract
)

signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# testing vars
INIT_SUPPLY = to_uint(1000)
Expand Down
4 changes: 2 additions & 2 deletions tests/token/erc20/test_ERC20_Upgradeable.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, to_uint, sub_uint, str_to_felt, assert_revert,
ActivatedSigner, to_uint, sub_uint, str_to_felt, assert_revert,
get_contract_def, cached_contract
)


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

USER = 999
INIT_SUPPLY = to_uint(1000)
Expand Down
4 changes: 2 additions & 2 deletions tests/token/erc721/test_ERC721_Mintable_Burnable.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, str_to_felt, ZERO_ADDRESS, TRUE, FALSE, assert_revert, INVALID_UINT256,
ActivatedSigner, str_to_felt, ZERO_ADDRESS, TRUE, FALSE, assert_revert, INVALID_UINT256,
assert_event_emitted, get_contract_def, cached_contract, to_uint, sub_uint, add_uint
)


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

NONEXISTENT_TOKEN = to_uint(999)
# random token IDs
Expand Down
5 changes: 3 additions & 2 deletions tests/token/erc721/test_ERC721_Mintable_Pausable.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, str_to_felt, TRUE, FALSE, get_contract_def, cached_contract, assert_revert, to_uint
ActivatedSigner, str_to_felt, TRUE, FALSE, get_contract_def, cached_contract,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this PR is old, but would that TRUE and FALSE end up in main if we merge this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that TRUE and FALSE end up in main as a duplicate you mean? If that's the question, I'll rebase first and make sure there are no duplicates

assert_revert, to_uint
)


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# random token IDs
TOKENS = [to_uint(5042), to_uint(793)]
Expand Down
4 changes: 2 additions & 2 deletions tests/token/erc721/test_ERC721_SafeMintable_mock.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, str_to_felt, ZERO_ADDRESS, INVALID_UINT256, assert_revert,
ActivatedSigner, str_to_felt, ZERO_ADDRESS, INVALID_UINT256, assert_revert,
assert_event_emitted, get_contract_def, cached_contract, to_uint
)


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# random token id
TOKEN = to_uint(5042)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import pytest
from starkware.starknet.testing.starknet import Starknet
from utils import (
Signer, str_to_felt, MAX_UINT256, get_contract_def, cached_contract,
ActivatedSigner, str_to_felt, MAX_UINT256, get_contract_def, cached_contract,
TRUE, assert_revert, to_uint, sub_uint, add_uint
)


signer = Signer(123456789987654321)
signer = ActivatedSigner(123456789987654321)

# random token IDs
TOKENS = [
Expand Down
Loading