After getting into Nano (RaiBlocks at the time), I decided to work on my own iOS light wallet. One of the first features I worked on was deriving the same Nano account address from a seed generated by raiwallet.com. When writing my own Swift implementation, I referenced raiwallet.com’s source code as well as a light Python wallet.
Going from a seed to Nano address:
- Generate secret key from seed
- Derive public key from secret key
- Derive Nano account address from public key
I’ve noticed at least a few times, some confusion on the difference between a public key and account address. For clarity, here are my definitions:
seed: A random 32-byte value. This can be any random data. Typically seen as a 64-character hex string. Keep it secret!
secret key: Also called a private key. Keyp this key secret as well since it’s responsible for signing blocks.
public key: The key from which your account address is derived from. The public key can be shared.
account address: An address derived from a public key. Base-32 encoded with a checksum and ‘xrb_’ prepended.
Libraries
- libsodium (NaCl)
- swift-sodium
Generating a Secret Key
A secret key in this context is just a Blake2b hash of a 32-byte hex value (your seed) along with a 4-byte seed index value. I spent a frustrating few seconds wondering why my secret key was coming out different after hashing the seed and index — turned out that my index byte count was 32 instead of 4 lol.
The seed index should start at 0, and should be incremented each time a user creates a new account address off of the same seed. With an unsigned 32-bit integer, you can generate 2³²-1 unique key pairs.
Note: swift-sodium’s genericHash class uses Blake2b under the hood.
Deriving Public from Secret
Modify Ed25519
Nano uses Ed25519 key pair/signing functions with Blake2b internal hashing, which is different than libsodium’s internal SHA512.
I added an extra function to libsodium’s keypair.c to be able to derive the public from a secret key:
Calling it in Swift:
XRB Account Address
The XRB account address is composed of 3 parts:
- ‘xrb_’
- encoded public key
- encoded checksum
‘xrb_’<encoded public key><encoded checksum>
Encoding
Each 5-bit chunk in the integer is the index of a character in the 32 letter encoding alphabet ‘13456789abcdefghijkmnopqrstuwxyz’. You can create an encoding lookup table where numbers 00000–11111 (0–31) map to a letter in the alphabet:
Encoding an integer (or bit array) requires parsing in 5-bit chunks:
Public Key
Create a bit array from the public key byte array, and zero pad it with 4 extra bits (256 -> 260 bits). Because the first 4 bits of a public key are zero padded, the first letter of an account address can only either be a ‘1’ or a ‘3' (00000 : ‘1’, 00001 : ‘3’).
Checksum
Take a Blake2b hash of the public key with a 5 byte digest size, and perform a byte swap on the resulting hash. Encode the result.
Concatenate all 3 and you’ve got an address similar to the one below:
xrb_1sf5f67nw56ts998bsrnjhp4po3y9ky57mg6c4534rqbz96sczotn4nufhf7