Skip to content

Commit

Permalink
Support SNIP-9 (#1530)
Browse files Browse the repository at this point in the history
* Big things:
- Added new Transaction models: InvokeOutsideV1, InvokeOutsideV2
- Added method for SNIP9 nonce verification
- Added Account methods for population and broadcasting(execution) of InvokeOutside transactions

Small fixes:
- Allowed for incomplete definition of ParameterDict as `contains` fiels is often missing and linter is complaining.

WIP: miss docs and tests

* - Added outside execution model, defined hashing
- Extended base account interface
- Added all utilities to deal with SNIP-9 nonce and generating OutsideExecution call
- Bugfix type data generation
- Cleaned up as much as I could to keep changes minimal
- Added docs
- Added test for positive scenario
- Added one test for wrong caller scenario

* * lint
* test fixes

* ok. so I am not allowed to change ci configs

* fixing tests

* lint

* switched back to increase balance

* tiny revert

* comment documentation test to check if that is what affects test account balance

* Revert "comment documentation test to check if that is what affects test account balance"

This reverts commit 5851f12.

* change for another call in documentation. removed account deployment.

* fix

* fix doctest

* more fixes

* remove balance change calls

* Fixed naming

* fix documentation

* added secrets

* trigger build

* Review comments fixws

* linter

* fixup

* Added comment

* fix

* lost empty line

* comments fixes

* Update starknet_py/constants.py

Co-authored-by: Franciszek Job <[email protected]>

* comment fixupi

* fix wordings

* fix

* fixes

* more fixes

* more fixes

* rename

* Fix

* linter

* Update starknet_py/tests/e2e/docs/guide/test_account_sign_outside_transaction.py

Co-authored-by: Franciszek Job <[email protected]>

* Update starknet_py/tests/e2e/docs/guide/test_account_sign_outside_transaction.py

Co-authored-by: Franciszek Job <[email protected]>

* Update starknet_py/net/account/base_account.py

Co-authored-by: Franciszek Job <[email protected]>

* a bit more review comment fixes

* revert execute_v1

* remove auto fee estimation

* Update docs/guide/account_and_client.rst

Co-authored-by: Franciszek Job <[email protected]>

* Update starknet_py/tests/e2e/docs/guide/test_account_sign_outside_transaction.py

Co-authored-by: Franciszek Job <[email protected]>

* import fix

* fix doctest

* Update starknet_py/tests/e2e/account/outside_execution_test.py

Co-authored-by: Franciszek Job <[email protected]>

* Update starknet_py/tests/e2e/account/outside_execution_test.py

Co-authored-by: Franciszek Job <[email protected]>

* changelog

* Update docs/migration_guide.rst

Co-authored-by: Franciszek Job <[email protected]>

* fixing fixes

---------

Co-authored-by: Franciszek Job <[email protected]>
  • Loading branch information
baitcode and franciszekjob authored Dec 16, 2024
1 parent 3996323 commit a8d7353
Show file tree
Hide file tree
Showing 15 changed files with 577 additions and 25 deletions.
11 changes: 11 additions & 0 deletions docs/guide/account_and_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ Account also provides a way of creating signed transaction without sending them.
:language: python
:dedent: 4

Outside execution
-----------------

Outside execution allows a protocol to submit a transaction on behalf of another account. This feature is implemented according to `SNIP-9 <https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md>`_.

Account also provides a way of signing transaction which later can be execute by another account. Signer does not need to be funded with tokens as executor will pay the fee.

.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_account_sign_outside_transaction.py
:language: python
:dedent: 4

Multicall
---------

Expand Down
2 changes: 2 additions & 0 deletions docs/migration_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Migration guide

1. Added :class:`NonZeroType` in order to fix parsing ABI which contains Cairo`s `core::zeroable::NonZero <https://github.com/starkware-libs/cairo/blob/a2b9dddeb3212c8d529538454745b27d7a34a6cd/corelib/src/zeroable.cairo#L78>`_

2. Added `SNIP-9 <https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md>`_ support to :class:`~starknet_py.net.account.account.Account`. Now it's possible to create a :class:`~starknet_py.net.client_models.Call` for outside execution using :meth:`~starknet_py.net.account.account.Account.sign_outside_execution_call`.

******************************
0.24.3 Migration guide
******************************
Expand Down
10 changes: 10 additions & 0 deletions starknet_py/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from enum import IntEnum
from pathlib import Path

# Address came from starkware-libs/starknet-addresses repository: https://github.com/starkware-libs/starknet-addresses
Expand Down Expand Up @@ -45,3 +46,12 @@
PUBLIC_KEY_RESPONSE_LENGTH = 65
SIGNATURE_RESPONSE_LENGTH = 65
VERSION_RESPONSE_LENGTH = 3

# Result of `encode_shortstring("ANY_CALLER")`
ANY_CALLER = 0x414E595F43414C4C4552


# OUTSIDE EXECUTION INTERFACE_VERSION with ID
class OutsideExecutionInterfaceID(IntEnum):
V1 = 0x68CFD18B92D1907B8BA3CC324900277F5A3622099431EA85DD8089255E4181
V2 = 0x1D1144BB2138366FF28D8E9AB57456B1D332AC42196230C3A602003C89872
121 changes: 121 additions & 0 deletions starknet_py/hash/outside_execution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from starknet_py.constants import OutsideExecutionInterfaceID
from starknet_py.net.client_models import OutsideExecution
from starknet_py.net.schemas.common import Revision
from starknet_py.utils.typed_data import TypedData

OUTSIDE_EXECUTION_INTERFACE_ID_TO_TYPED_DATA_REVISION = {
OutsideExecutionInterfaceID.V1: Revision.V0,
OutsideExecutionInterfaceID.V2: Revision.V1,
}


# TODO(#1537): Implement as method of OutsideExecution
def outside_execution_to_typed_data(
outside_execution: OutsideExecution,
outside_execution_version: OutsideExecutionInterfaceID,
chain_id: int,
) -> TypedData:
"""
SNIP-12 Typed Data for OutsideExecution implementation. For revision V0 and V1.
"""

revision = OUTSIDE_EXECUTION_INTERFACE_ID_TO_TYPED_DATA_REVISION[
outside_execution_version
]

if revision == Revision.V0:
return TypedData.from_dict(
{
"types": {
"StarkNetDomain": [
{"name": "name", "type": "felt"},
{"name": "version", "type": "felt"},
{"name": "chainId", "type": "felt"},
],
"OutsideExecution": [
{"name": "caller", "type": "felt"},
{"name": "nonce", "type": "felt"},
{"name": "execute_after", "type": "felt"},
{"name": "execute_before", "type": "felt"},
{"name": "calls_len", "type": "felt"},
{"name": "calls", "type": "OutsideCall*"},
],
"OutsideCall": [
{"name": "to", "type": "felt"},
{"name": "selector", "type": "felt"},
{"name": "calldata_len", "type": "felt"},
{"name": "calldata", "type": "felt*"},
],
},
"primaryType": "OutsideExecution",
"domain": {
"name": "Account.execute_from_outside",
"version": "1",
"chainId": str(chain_id),
"revision": Revision.V0,
},
"message": {
"caller": outside_execution.caller,
"nonce": outside_execution.nonce,
"execute_after": outside_execution.execute_after,
"execute_before": outside_execution.execute_before,
"calls_len": len(outside_execution.calls),
"calls": [
{
"to": call.to_addr,
"selector": call.selector,
"calldata_len": len(call.calldata),
"calldata": call.calldata,
}
for call in outside_execution.calls
],
},
}
)

# revision == Revision.V1
return TypedData.from_dict(
{
"types": {
"StarknetDomain": [
{"name": "name", "type": "shortstring"},
{"name": "version", "type": "shortstring"},
{"name": "chainId", "type": "shortstring"},
{"name": "revision", "type": "shortstring"},
],
"OutsideExecution": [
{"name": "Caller", "type": "ContractAddress"},
{"name": "Nonce", "type": "felt"},
{"name": "Execute After", "type": "u128"},
{"name": "Execute Before", "type": "u128"},
{"name": "Calls", "type": "Call*"},
],
"Call": [
{"name": "To", "type": "ContractAddress"},
{"name": "Selector", "type": "selector"},
{"name": "Calldata", "type": "felt*"},
],
},
"primaryType": "OutsideExecution",
"domain": {
"name": "Account.execute_from_outside",
"version": "2",
"chainId": str(chain_id),
"revision": Revision.V1,
},
"message": {
"Caller": outside_execution.caller,
"Nonce": outside_execution.nonce,
"Execute After": outside_execution.execute_after,
"Execute Before": outside_execution.execute_before,
"Calls": [
{
"To": call.to_addr,
"Selector": call.selector,
"Calldata": call.calldata,
}
for call in outside_execution.calls
],
},
}
)
140 changes: 132 additions & 8 deletions starknet_py/net/account/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union

from starknet_py.common import create_compiled_contract, create_sierra_compiled_contract
from starknet_py.constants import FEE_CONTRACT_ADDRESS, QUERY_VERSION_BASE
from starknet_py.constants import (
ANY_CALLER,
FEE_CONTRACT_ADDRESS,
QUERY_VERSION_BASE,
OutsideExecutionInterfaceID,
)
from starknet_py.hash.address import compute_address
from starknet_py.hash.outside_execution import outside_execution_to_typed_data
from starknet_py.hash.selector import get_selector_from_name
from starknet_py.hash.utils import verify_message_signature
from starknet_py.net.account.account_deployment_result import AccountDeploymentResult
from starknet_py.net.account.base_account import BaseAccount
from starknet_py.net.account.base_account import (
BaseAccount,
OutsideExecutionSupportBaseMixin,
)
from starknet_py.net.client import Client
from starknet_py.net.client_models import (
Call,
Calls,
EstimatedFee,
Hash,
OutsideExecution,
OutsideExecutionTimeBounds,
ResourceBounds,
ResourceBoundsMapping,
SentTransactionResponse,
Expand All @@ -40,21 +51,21 @@
from starknet_py.net.signer import BaseSigner
from starknet_py.net.signer.key_pair import KeyPair
from starknet_py.net.signer.stark_curve_signer import StarkCurveSigner
from starknet_py.serialization.data_serializers.array_serializer import ArraySerializer
from starknet_py.serialization.data_serializers.felt_serializer import FeltSerializer
from starknet_py.serialization.data_serializers.payload_serializer import (
from starknet_py.serialization.data_serializers import (
ArraySerializer,
FeltSerializer,
PayloadSerializer,
)
from starknet_py.serialization.data_serializers.struct_serializer import (
StructSerializer,
UintSerializer,
)
from starknet_py.utils.iterable import ensure_iterable
from starknet_py.utils.sync import add_sync_methods
from starknet_py.utils.typed_data import TypedData


# pylint: disable=too-many-public-methods,disable=too-many-lines
@add_sync_methods
class Account(BaseAccount):
class Account(BaseAccount, OutsideExecutionSupportBaseMixin):
"""
Default Account implementation.
"""
Expand Down Expand Up @@ -291,6 +302,55 @@ async def get_nonce(
self.address, block_hash=block_hash, block_number=block_number
)

async def _check_outside_execution_nonce(
self,
nonce: int,
*,
block_hash: Optional[Union[Hash, Tag]] = None,
block_number: Optional[Union[int, Tag]] = None,
) -> bool:
(is_valid,) = await self._client.call_contract(
call=Call(
to_addr=self.address,
selector=get_selector_from_name("is_valid_outside_execution_nonce"),
calldata=[nonce],
),
block_hash=block_hash,
block_number=block_number,
)
return bool(is_valid)

async def get_outside_execution_nonce(self, retry_count=10) -> int:
while retry_count > 0:
random_stark_address = KeyPair.generate().public_key
if await self._check_outside_execution_nonce(random_stark_address):
return random_stark_address
retry_count -= 1
raise RuntimeError("Failed to generate a valid nonce")

async def _get_outside_execution_version(
self,
) -> Union[OutsideExecutionInterfaceID, None]:
for version in [
OutsideExecutionInterfaceID.V1,
OutsideExecutionInterfaceID.V2,
]:
if await self.supports_interface(version):
return version
return None

async def supports_interface(
self, interface_id: OutsideExecutionInterfaceID
) -> bool:
(does_support,) = await self._client.call_contract(
Call(
to_addr=self.address,
selector=get_selector_from_name("supports_interface"),
calldata=[interface_id],
)
)
return bool(does_support)

async def get_balance(
self,
token_address: Optional[AddressRepresentation] = None,
Expand Down Expand Up @@ -345,6 +405,56 @@ async def sign_invoke_v1(
signature = self.signer.sign_transaction(execute_tx)
return _add_signature_to_transaction(execute_tx, signature)

async def sign_outside_execution_call(
self,
calls: Calls,
execution_time_bounds: OutsideExecutionTimeBounds,
*,
caller: AddressRepresentation = ANY_CALLER,
nonce: Optional[int] = None,
interface_version: Optional[OutsideExecutionInterfaceID] = None,
) -> Call:
if interface_version is None:
interface_version = await self._get_outside_execution_version()

if interface_version is None:
raise RuntimeError(
"Can't initiate call, outside execution is not supported."
)

if nonce is None:
nonce = await self.get_outside_execution_nonce()

outside_execution = OutsideExecution(
caller=parse_address(caller),
nonce=nonce,
execute_after=execution_time_bounds.execute_after_timestamp,
execute_before=execution_time_bounds.execute_before_timestamp,
calls=list(ensure_iterable(calls)),
)
chain_id = await self._get_chain_id()
signature = self.signer.sign_message(
outside_execution_to_typed_data(
outside_execution, interface_version, chain_id
),
self.address,
)
selector_for_version = {
OutsideExecutionInterfaceID.V1: "execute_from_outside",
OutsideExecutionInterfaceID.V2: "execute_from_outside_v2",
}

return Call(
to_addr=self.address,
selector=get_selector_from_name(selector_for_version[interface_version]),
calldata=_outside_transaction_serialiser.serialize(
{
"outside_execution": outside_execution.to_abi_dict(),
"signature": signature,
}
),
)

async def sign_invoke_v3(
self,
calls: Calls,
Expand Down Expand Up @@ -890,3 +1000,17 @@ def _parse_calls_cairo_v1(calls: Iterable[Call]) -> List[Dict]:
calls=ArraySerializer(_call_description_cairo_v1),
)
)
_outside_transaction_serialiser = StructSerializer(
OrderedDict(
outside_execution=StructSerializer(
OrderedDict(
caller=FeltSerializer(),
nonce=FeltSerializer(),
execute_after=UintSerializer(bits=64),
execute_before=UintSerializer(bits=64),
calls=ArraySerializer(_call_description_cairo_v1),
)
),
signature=ArraySerializer(FeltSerializer()),
)
)
Loading

0 comments on commit a8d7353

Please sign in to comment.