Wallets and keys
The blockchain world is heavily based on private key cryptography. Operations you perform on chain generally need to be signed using a private key and will be verified against (some version of) the corresponding public key. But the details are confusing, and the terminology can be misleading. Let's break down the process from start to finish.
Cryptographic primitives
Private key cryptography allows for two pairs of operations:
- Encrypting a message using a public key, and decrypting it using the corresponding private key.
- Signing a message using a private key, and validating the signature using the corresponding public key.
Encryption is very rarely used in blockchains. Instead, signing of messages is the primary feature we use from private key cryptography. There are lots of different algorithms out there, but a bit of pseudocode will explain the high level concepts pretty well:
myPrivateKey := something // we'll explain where this comes from below
myPublicKey := derivePublicKey(myPrivateKey)
someMessage := b"deadbeef" // any arbitrary binary payload
signature := signMessage(someMessage, myPrivateKey)
isValid := validateSignature(someMessage, signature, myPublicKey)
The point here is that you can safely share your public key with the rest of the world, and they cannot figure out what the corresponding private key is (at least before the heat death of the universe, assuming the cryptography is well designed). Then, using my private key, I can prove that only someone who controls that private key sent a message. And anyone in the world can validate it.
For the blockchain, this is the basis for signing transactions and sending funds. If I have 2 BTC, and I want to send 1 BTC to Alice, I can sign a transaction using my private key, and the network will know that the owner of the 2 BTC created that transaction.
You might challenge this, and say that the private key could be hacked. This is true, and people lose funds like this all the time. This is another "feature" of the blockchain: whoever controls the keys controls the funds. You may have heard a similar phrase: not your keys, not your coins. This is a big difference between blockchain and TradFi (traditional finance). If someone hacks into my bank account and sends all my money to someone else, I'll probably be able to get the money back. On the blockchain, if they have your keys, your money is gone.
But we still don't know where the private keys come from. One possibility is just generating a random private key. But that's not normally what happens in the blockchain space. Instead, we have...
Cryptographic hash functions
Another cryptographic primitive we use is a hash function. The idea of a hash function is to take an arbitrary amount of data and generate a fixed number of bytes. For example, SHA256 generates (surprise) a 256 bit value. The goals of hash functions are:
- Non-reversable: based on a hashed value, it should be impossible to determine the input data that led to it.
- Even distribution: there should be roughly an equal chance of getting any possible value from the hash function.
- Cascading: a small change in input should result in a large change in the output.
Hashes are used quite extensively in blockchains, such as "transaction hashes" and in the process of signing messages described above. Our use of hash functions in this document revolves around generating wallet addresses, discussed below.
Seed phrases
A seed phrase is usually a set of 12 or 24 English words, taken from a dictionary of 1,024 words available. Seed phrases are specified by BIP-39, if you want to look up more details. Using that set of words, you can generate a large number. This number can then be used to derive a private key, using...
Derivation paths
Another part of the BIP-39 standard are derivation paths. These look like m/44'/118'/0'/0/0. You can see more information on them in BIP-44. But the basic idea is that you can take that big number from the seed phrase and generate a large number of different private keys.
The different numbers in the derivation path can be used to indicate different coin types. The 118 above, for example, is the default coin type in the Cosmos ecosystem. 60 is used in the Ethereum space, by contrast. (And, since Injective follows Ethereum standards, it's used by Injective too.) For a full list, check SLIP-44.
Other numbers in that list can be used for deriving a wider range of wallets within the same coin type. In particular, the last 0 is typically called the index, and can be used for creating numbered accounts. Some common use cases:
- Having a single seed phrase for bots, but allowing the bots to manage multiple wallets.
- Using a single hardware wallet (like a Ledger) but managing multiple accounts.
bech32
bech32 is a standard for representing binary data. It's not worth going into the details of how bech32 works here, but the important point is that it provides for a human readable part (HRP) and a payload, and that the payload includes a checksum to avoid transcription errors. bech32 is used throughout the Cosmos ecosystem for encoding addresses. (It started in the Bitcoin world as SegWit addresses.)
You can use an online encoder to get a feel for this. As an example, if I specify an HRP of fpco and a payload of deadbeef, I get the wallet address fpco1m6kmamch637yv. Note that the 1 here represents the separation between the HRP and the encoded data.
The question is: what data should we use for the payload? The obvious answer there would be the public key. Unfortunately public keys are large, and embedding an entire public key in the wallet address wouldn't scale well.
Instead, we generate a wallet address by first hashing the public key, and then bech32 encoding it. Different ecosystems use different standards for this hashing, in particular Injective is different from the rest of Cosmos. You can check out the implementation in cosmos-rs for details.
Which leaves the final question...
Signatures
There's a problem with using hashed public keys for wallet addresses: we can't use them for signature validation! How can we be sure that a transaction originated from the right person if we can't validate the signature?
The answer is that, when you send a transaction, you have to not only include the wallet address (the bech32-encoded, hashed public key), but also the raw public key. Then the blockchain is responsible for confirming that the public key would result in the given wallet address, and that the attached signature matches that public key.
Playing with this
Everything described here is pretty high level and abstract, but hopefully enough to orient you. If you want to experiment, virtually every wallet software out there will generate seed phrases and wallet addresses for you. You can also do this with the cosmos CLI tool from cosmos-rs:
➜ cosmos wallet gen-wallet osmo
Mnemonic: buffalo comic shock alarm table urge huge crucial crystal february twice will path comfort afford differ come cage despair hawk must talk thing trumpet
Address: osmo14aqvzkjewk4gjp9uq02536v02695fpe4m5ukpt
➜ cosmos wallet change-address-type osmo14aqvzkjewk4gjp9uq02536v02695fpe4m5ukpt fakehrp
fakehrp14aqvzkjewk4gjp9uq02536v02695fpe426v9sc
➜ cosmos wallet change-address-type fakehrp14aqvzkjewk4gjp9uq02536v02695fpe426v9sc osmo
osmo14aqvzkjewk4gjp9uq02536v02695fpe4m5ukpt