Ledger Accounts

The ledger is canister on the IC that is so important that the dfx tool has dedicated commands for calling its methods:

$ dfx ledger help

On the IC, the ledger has the principal ryjl3-tyaaa-aaaaa-aaaba-cai, which is a checksummed and encoded form of:

00000000000000020101

Being a canister, its principal must end with 0x01. The low value of the remainder of the ID indicates it was one of the first canisters deployed on the IC.

We can examine the ledger in the same way as any other canister:

$ dfx canister --network ic info ryjl3-tyaaa-aaaaa-aaaba-cai
Controllers: r7inp-6aaaa-aaaaa-aaabq-cai
Module hash: 0x61a312e4bf4f86899edabf6b3cedc504b2b0825d9b7956abea65582095692a9a

Local Ledgers

To avoid moving real tokens around, during development, we run a local ledger canister.

The dfx ledger commands ordinarily call canister with the true ledger’s ID. Later versions of dfx support the --ledger-canister-id option to override this.

If we forget to record the local ledger’s ID on deployment, we can look it up:

$ cat .dfx/local/canister_ids.json

For example:

  "ledger": {
    "local": "rrkah-fqaaa-aaaaa-aaaaq-cai"
  },

We define a couple of aliases:

$ LEDGER="rrkah-fqaaa-aaaaa-aaaaq-cai"
$ alias balance="dfx ledger balance --ledger-canister-id $LEDGER"
$ alias transfer="dfx ledger transfer --ledger-canister-id $LEDGER"

Assuming we’ve followed the instructions for deploying the local ledger and that we’ve given 1000 ICP to the user default:

$ balance default
1000.00000000 ICP

Account IDs

We can run dfx to find the default account number of a user. We demonstrate on our zoo account:

$ dfx identity use zoo
$ dfx ledger account-id
e77eab8d01a5102d67bbc0b3ba120151f005debecdeadfb3c91f4b4fe804db21

We transfer 42 ICP from default to zoo:

$ dfx identity use default
$ transfer e77eab8d01a5102d67bbc0b3ba120151f005debecdeadfb3c91f4b4fe804db21 --amount 42 --memo 0
$ balance e77eab8d01a5102d67bbc0b3ba120151f005debecdeadfb3c91f4b4fe804db21
42.00000000 ICP

Let’s compute this account ID manually. It is derived from a principal (which is why the account-id command has no need for --ledger-canister-id) as follows:

  1. Concatenate "\naccount-id" with the principal (without a checksum).

  2. Append 32 zero bytes.

  3. Compute the SHA224 hash.

  4. Prepend its CRC-32 checksum.

Recall the principal of the zoo key is 4ff2c79f70067d24bbba4a164737e0eddd62802ac603531fa0fc855b02. Then its raw unchecksummed default account ID is:

$ (printf "\naccount-id"; (echo 4ff2c79f70067d24bbba4a164737e0eddd62802ac603531fa0fc855b02 ; printf %064d 0) | xxd -r -p) | sha224sum
01a5102d67bbc0b3ba120151f005debecdeadfb3c91f4b4fe804db21

We abuse gzip to compute its CRC-32 checksum:

$ echo 01a5102d67bbc0b3ba120151f005debecdeadfb3c91f4b4fe804db21 | xxd -r -p | gzip | tail -c 8 | head -c 4 | xxd -p | tac -rs .. ; echo

e77eab8d

Thankfully, this agrees with dfx ledger account-id.

Subaccounts

Above, in step 2, instead of 32 zero bytes, we can in fact use any arbitrary 32 bytes, known as the subaccount, resulting in an account we control. The zero subaccount is simply the default.

Subaccounts are related to salts in the context of password hashes. If we supply a subaccount along with the account derived from our principal and subaccount, then the ledger lets us operate on the account.

In effect, we have as many accounts as we want. We can use a fresh one for each new application, making it harder for independent transactions to stomp over each other.


💡