We can create an account by wallet like MetaMask or StarMask, but I’m curious about how an account is created on the blockchain system.

As a wallet has been embedded in the starcoin node, we can use it to create an account as follow:

$ ./target/debug/starcoin -d ~/.starcoin -n dev account create -p my-pass
{
  "ok": {
    "address": "0x2f1aeb63bd30d8eb841d6a941c5d6df3",
    "is_default": false,
    "is_readonly": false,
    "public_key": "0x91f79bdd9ced49332bf85b751d02339e05aff047c386d0c14b380d8519d2fb4b",
    "receipt_identifier": "stc1p9udwkcaaxrvwhpqad22pchtd7vy2276p"
  }
}

As above we can see our account has been created, and the address is: 0x2f1aeb63bd30d8eb841d6a941c5d6df3. We’ll find some files are created, if we check our local directory at ~/.starcoin/dev:

$ ls -l ~/.starcoin/dev

drwxr-xr-x  10 wh  staff    320 Jun 16 18:32 account_vaults
-rw-r--r--   1 wh  staff    257 Jun 16 18:32 config.toml
-rw-r--r--   1 wh  staff  89048 Jun 16 18:32 genesis
-rw-r--r--   1 wh  staff  10710 Jun 16 18:32 genesis_config.json
-rw-------   1 wh  staff     64 Jun 16 18:32 network_key
srw-------   1 wh  staff      0 Jun 16 18:32 starcoin.ipc
-rw-r--r--   1 wh  staff  14678 Jun 16 18:32 starcoin.log
drwxr-xr-x   3 wh  staff     96 Jun 16 18:32 starcoindb

We’ll meet them later, for now, let’s follow the source code to find how an account is created.

Two Account Provider Strategies

From the code here we can see:

  • If there is a account_dir configured, then it should be AccountProviderStrategy::Local
  • Otherwise AccountProviderStrategy::RPC is used.

As we specify the -d ~/.starcoin to run above command to create an account, so I think the account_dir could be ~/.starcoin/dev/account_vaults. Just simply add a print statement in account/provider/src/provider/mod.rs, and remove the directory, then build & run it again:

  $ rm -rf ~/.starcoin/dev/
  $ cargo build
  $ ./target/debug/starcoin -d ~/.starcoin -n dev account create -p my-pass
 +++++++  RPC
{
  "ok": {
    "address": "0x7760d5c787b9918005d82cca6d8225a9",
    "is_default": false,
    "is_readonly": false,
    "public_key": "0xa8459efd0f6becd563bca5ea6d8b930e9f6f19a30aef1b9632baa62f59e2a21d",
    "receipt_identifier": "stc1pwasdt3u8hxgcqpwc9n9xmq394y0qwh24"
  }
}

Emmm, things don’t always happen as your thoughts, aren’t they? AccountProviderStrategy only will be used if we run command and pass --local-account-dir option:

  $ ./target/debug/starcoin -d ~/.starcoin/ --local-account-dir=~/.starcoin/dev/account_vaults/ -n dev account create -p my-pass
  +++++++  LOCAL
{
  "ok": {
    "address": "0x18d44098682869343f232d6cf0bdf9a9",
    "is_default": true,
    "is_readonly": false,
    "public_key": "0x17e9deb0b3784b5abebad3a7cb8030c23360d6b7f8d108a8186d20e2a23b51ff",
    "receipt_identifier": "stc1prr2ypxrg9p5ng0er94k0p00e4yhnhgw0"
  }
}

There it is.

AccountManager & AccountStorage

I’m tracing the invocation path, and find that no matter which strategy is used, then eventually, the AccountManager is used to do the actually job. And it mainly relys on AccountStorage.

Now let’s check the defination of AccountManager.create_account:

pub fn create_account(&self, password: &str) -> AccountResult<Account> {
    let private_key = gen_private_key();
    let private_key = AccountPrivateKey::Single(private_key);
    let address = private_key.public_key().derived_address();
    self.save_account(
        address,
        private_key.public_key(),
        Some((private_key, password.to_string())),
    )
}

It’s very simple, just generate a private key, and use it’s public key to derive an address, that’s how your account address comes out.

Two more invocations are involved: generate_private_key and AccountManager.save_account. Let’s check the first one:

generate_private_key

pub(crate) fn gen_private_key() -> Ed25519PrivateKey {
    let mut seed_rng = rand::rngs::OsRng;
    let seed_buf: [u8; 32] = seed_rng.gen();
    let mut rng: StdRng = SeedableRng::from_seed(seed_buf);
    Ed25519PrivateKey::generate(&mut rng)
}

Emmm, we are using Ed25519, as konwn as EdDSA(Edwards-curve Digital Signature Algorithm).

Then how an address is derived?

As we can see, the address is derived by a public key, which defined in starcoin_vm_types::transaction::authenticator::AccountPublicKey.derived_address. The main function of this invocation is use starcoin_vm_types::transaction::authenticator::AuthenticationKey::from_preimage create and AuthenticationKey can call derived_address that bind to it:


/// Create an authentication key from a preimage by taking its sha3 hash
pub fn from_preimage(preimage: &AuthenticationKeyPreimage) -> AuthenticationKey {
    AuthenticationKey::new(*HashValue::sha3_256_of(&preimage.0).as_ref())
}

/// Return an address derived from the last `AccountAddress::LENGTH` bytes of this
/// authentication key.
pub fn derived_address(&self) -> AccountAddress {
    // keep only last 16 bytes
    let mut array = [0u8; AccountAddress::LENGTH];
    array.copy_from_slice(&self.0[Self::LENGTH - AccountAddress::LENGTH..]);
    AccountAddress::new(array)
}

How to avoid collision of address?

TODO

How is an Account be Saved?

It’s mainly implemented in AccountManager.save_account:

  1. Check existence.
  2. Verify private key and password.
  3. Invoke Account::create to create the account: store it to corresponding place, and change the corresponding settings.
  4. Add to store.
  5. Set it to default, if it’s the first address.

Conclusion

So, an account has been created. There is no magic happens, which means no on chain operations are involved. Just generates a private key and public key pair, and derives an address.

The key pair is just a way that proof you own the address. So on the blockchain, you can send coins to any address, if nobody can proof that his owns that address, then the coins some how is lost forever.