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
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.