Signing transactions and off-chain messages

Since several people have asked us if Argent X is supporting EIP-712 I guess this thread does not have the visibility that it should :slight_smile:

I’m thus posting here the content of the Github discussion linked above which is a proposal for a version of EIP-712 adapted to StarkNet.

Note that this is already implemented in starknet.js and Argent X.

Context

Wallets have to generate signatures for 2 different types of data: transactions that will be sent to an Account Contract (AC) via the sequencer, and off-chain messages that will be returned to the requesting dapp for verification by calling the is_valid_signature method on the AC.

It is essential that there is no collision between the two sets of signatures, or, in other words, we must ensure that a signature generated on the encoded data of an off-chain message can never be a valid signature on the encoded data of a transaction.

In Ethereum the encoded data of a transaction is clearly defined as RLP<nonce, gasPrice, startGas, to, value, data> and one can easily satisfy the no-collision requirement by starting the encoded data of an off-chain message by 0x19 since it can never be the first byte of RLP encoded data. This is specified in EIP-191.

In addition, a good encoding scheme should make it easy for the wallet to clearly display to the user what he is signing. In Ethereum, this is specified in EIP-712.

On Starknet we have Account Abstraction, which means that the data to be signed for a transaction is (at least for now) completely open.

Proposition

We define the 2 short string prefixes:

  • PREFIX_TRANSACTION = 'StarkNet Transaction'
  • PREFIX_MESSAGE = 'StarkNet Message'

and the encoding:

Enc[X=(x0, x1, …, xn-1)] = Enc[Enc[x0], Enc[x1], …, Enc[xn-1]] = h(…h(h(0,Enc[x0]), Enc[x1]), …), n)

when X is an array, and:

Enc = x

when x is a felt, where

h(a,b)

is the Pedersen hash on 2 field elements.

We also define

selector(str) = starknet_keccak(str)

as defined in cairo-lang/src/starkware/starknet/public/abi.py at master · starkware-libs/cairo-lang · GitHub

Transaction

As part of the discussions around the Account Contract interface, it was decided that AC will expose a single entry point for the execution of transactions:

@external
func execute(
        to: felt,
        selector: felt,
        calldata_len: felt,
        calldata: felt*,
        nonce: felt
    ) -> (response : felt):
end

For the moment there exists 2 implementations of Account Contracts:

Based on the above, the data to sign for a transaction from an AC calling the function method(params1, params2, ..., paramsN) on the contract to can be defined as:

signed_data = Enc[PREFIX_TRANSACTION, account, to, selector(‘method’), Enc[[params1, params2, …, paramsN]], nonce]

Off-chain message

Inspired by EIP-712 we can define the encoding of an off-chain message as:

signed_data = Enc[PREFIX_MESSAGE, domain_separator, account, hash_struct(message)]

where

  • domain_separator is defined below.
  • account is the Account Contract for which the signing is executed
  • hash_struct(message) is defined below.

hash_struct:

The message to be hashed is represented as a struct:

struct MyStruct:
    member param1: felt
    member param2: felt*
   ...
end

and we define its encoding as

hash_struct(message) = Enc[type_hash(MyStruct), Enc[param1], Enc[param2], …, Enc[paramN]]

where type_hash is defined as in EIP-712 (but using selector instead of keccak):

type_hash(MyStruct) = selector(‘MyStruct(params1:felt, params2:felt*,…)’)

If MyStruct references other struct types (and these in turn reference even more struct types), then the set of referenced struct types is collected, sorted by name and appended to the encoding. See EIP-712 for more details

domain_separator:

The domain_separator is defined as the hash_struct of the StarkNetDomain struct:

struct StarkNetDomain:
    member name: felt = 'www.myDapp.com'
    member version: felt = 1
    member chain_id: felt = 'SN_GOERLI'
end

where chain_id is one of ['SN_GOERLI', 'SN_MAIN'] in short strings.

Reference implementation

Some rationale elements

  • The 2 prefixes PREFIX_TRANSACTION and PREFIX_MESSAGE ensure that there will be no collisions between signatures for transactions and off-chain messages.
  • The domain_separator in the signed data of off-chain messages ensures that the signatures requested by a dapp cannot be used on another dapp, or on the same dapp but on another deployment (e.g. from Goërli to mainnet).
  • The account in the signed data of off-chain messages ensures that if several Account Contracts use the same key, the signature requested by a dapp for an account cannot be used to authenticate another account.
10 Likes