Objective
On Ethereum the address of an EOA account is derived from the associated public key. And multiple keys can be derived from a single secret (seed phrase).
There is thus a deterministic relation between this unique secret and a sequence of accounts, i.e. if you know the secret you can derive a sequence of account addresses.
There is also a deterministic relation between this unique secret and the private keys controlling this sequence of accounts, i.e. if you know the secret you can derive the sequence of private keys controlling these accounts.
This allows for great interoperability between wallets as one can simply import the secret from one wallet to another and immediately recover the account addresses and the controlling private keys.
On StarkNet we have account abstraction which means that the address of an account is computed as a smart contract with no direct relation to the key(s) controlling the account. By default users must thus keep track of their account addresses, and loosing an account address means loosing access to the account.
Furthermore, the key(s) controlling the account can potentially be changed at any point in time depending on the logic implemented by the account.
The objective of this proposition is to allow for some interoperability between StarkNet wallets by standardising the relation between a unique secret and a sequence of accounts such that the addresses of the accounts can be deterministically recovered from the secret.
Depending on the account implementations it might also create a deterministic relation between the secret and the sequence of keys controlling these accounts.
Proposition
Step 1: Derive a sequence of starkPair
from a secret
A sequence of keys are derived from a unique secret
using the Hierarchical Deterministic (HD) Wallet standard:
We can use standard BIP-44 derivation path:
m /44' /9004' /0' /0 /index
We apply on the derived private key, a grinding method inspired by
In summary, for a given secret
and index
, the starkPair
is given by:
const masterNode = HDNode.fromSeed(secret)
const path = m/44'/9004'/0'/0/${index}
const childNode = masterNode.derivePath(path)
const groundKey = grindKey(childNode.privateKey)
const starkPair = ec.getKeyPair(groundKey)
Step 2: Initialise each account with a different starkPair
The address of an account on StarkNet is defined in
https://docs.starknet.io/docs/Contracts/contract-address/
contract_address := pedersen(
“STARKNET_CONTRACT_ADDRESS”,
caller_address,
salt,
pedersen(contract_code),
pedersen(constructor_calldata))
For each starkPair
derived in step 1 we can thus create a unique account by passing the public key in the constructor calldata.
We can make the account address recoverable is we require the account to emit a standard event upon creation:
@event
func account_created(account: felt, key: felt):
end
where
-
account
is the address of the created account (note that this is not technically needed since the account is emitting the event, but could make searching easier depending on how events will be indexed in the future) -
key
is the public key of thestarkPair
.
Since the signature of StarkNet events only depends on the method name, each implementation can decide to pass additional parameters to the account_created
event. The only requirement is that the first parameter be the account
and the second the key
.
Recovering account addresses
Given a secret
, a wallet can easily derive the starkPair
associated to index 0, 1, 2, 3, … and get all the event_created
events whose key
matches one of the derived keys. The wallet automatically has the list of associated account addresses.
Note
- This proposition is relatively loose in purpose. It does not try to agree on a common bytecode (e.g. using a standard Proxy) or a common method to compute the salt. This could be the work of extending standards targeting more specific use cases. Its main objective is to have a simple method to recover the sequence of account addresses with minimal input from the user (i.e. only the
secret
) for maximum interoperability. - While the goal of the standard is to recover account addresses from the
secret
, it can also recover the associated private keys for EOA like account implementation (see e.g. https://github.com/OpenZeppelin/cairo-contracts/blob/main/src/openzeppelin/account/Account.cairo) if the derived key is used as the signer. - There is no guarantee that a matching
account_created
event corresponds to the correct account, i.e. malicious contracts could emit the same event. However, wallets can perform additional checks on the account to identify the legit ones. - Since StarkNet uses CREATE2, it is possible to have gaps in the sequence of accounts. For example, there may be an account for index 0, 1, 3, 4. In such case account 2 was counterfactually created and it may not be possible to recover its address unless the bytecode, salt, and constructor arguments are fully known.