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.

Minting Cycles

Topping up cycles on a canister involves the ledger.

First, we transfer ICP to a certain subaccount of the cycle-minting canister: rkp4c-7iaaa-aaaaa-aaaca-cai. The subaccount is a representation of the target canister ID: the first byte is the length of the ID, followed by the raw principal, then as many zero bytes as needed to make 32 bytes. The memo field is set to 0x50555054 ("TPUP").

A successful transfer returns the block height at which the transaction executed. We convert the ICP to cycles by calling notify_top_up which takes an argument of type:

record
  { block_index : nat64
  ; canister_id : principal
  }

Canister creation is similar, except the memo is 0x43524541 ("CREA") and we call notify_create_canister, which takes an argument of type:

record
  { controller : principal
  ; block_index : nat64
  }

Ben Lynn 💡