Solana Theory
ℹ️ Legal disclaimer: None of the content expressed in this article should be considered financial or investment advice, and is not an endorsement for any particular crypto product, token, or asset. This article was written solely for educational purposes.
Objectives
- Understand Solana’s history and why Solana is unique compared to other layer-one protocols.
- Understand Solana’s Account Model and the nuances between account types.
- Understand how to read and alter blockchain state via the RPC JSON API.
- Build an application to demonstrate Solana knowledge.
Brief History
Once upon a time, there was a Russian-American Qualcomm engineer by the name of Anatoly Yakovenko who dreamed of a decentralized, universal state machine that could compete with the Nasdaq on the basis of execution time. In light of Broadcom making moves to acquire Qualcomm, Yakovenko’s employer, Yakovenko and a band of senior Qualcomm engineers left their beloved company to realize this high-performance permissionless network: they called it Loom and developed it in C++.
However, after a turn of events, Anatoly and his core team later pivoted to the current Solana branding and rebuilt the project in the Rust programming language for its comparative performance to C++, but with the added benefit of being type and memory safe. Yakovenko published the white paper for his idea in 2017, and it was not until March of 2020 that the first Solana block was created.
Solana’s Innovations
In order to create a truly decentralized network without sacrificing performance, Solana network architecture demanded numerous key innovations that allowed for the speedy processing, propagation, and consensus of network information. Below is a list of the most important innovations a brief overview on each:
-
Proof of History (PoH) can be described as “a clock before consensus.” PoH is not Solana’s consensus algorithm, but a process that enhances the PoS algorithm that Solana uses for consensus.
-
Sealevel is Solana’s runtime that is responsible for parallelizing the execution of smart contracts. Sealevel is a breakthrough for the fact that it makes Solana a mutli-threaded protocol, as opposed to Ethereum which is single-threaded (only one smart contract can change the state of the blockchain at a time); that being, “Sealevel can process thousands of contracts in parallel, using as many cores as are available to the validator,” (Yakovenko, 2019).
Smart contract execution can be parallelized because transactions specify all the state the transaction will read or write to ahead of time (more on this later). That being, Sealevel will parallelize transactions if they:
- Change non-overlapping segments of state on the blockchain.
- Read to any part of the blockchain.
-
Archivers is a distributed ledger store for petabytes of blockchain data. Archivers was inspired by Filecoin’s Proof of Replication, but is optimized for verifying large batches of information using a VDF (Verifiable Delay Function).
Archivers are hardware participants in the Solana network that are dedicated to storing state, not participating in consensus. Archivers store information by going through the following process:
- An Archiver signals to the Solana network that they have X bytes of space for storing data.
- The network divides the ledger history into pieces to target some replication rate and fault tolerance.
- Once data assignments are made, the Archiver downloads their assigned data from consensus validators.
- Archivers must complete a Proof of Replication to verify that they’re storing data. They are rewarded ~3% of inflation for their efforts.
Verifiable Delay Functions
Solana’s Proof of History is a simple implementation of a verifiable delay function (VDF), which is a cryptographic primitive that allows us to verify that a certain amount of time has elapsed.
The Account Model
Every entity stored on the Solana blockchain is called an account. You can equate a Solana account to a file in your favorite OS (i.e., Linux) for the way that accounts similarly store arbitrary data and persist beyond the lifetime of a program.
Types of accounts:
- Data accounts — used for storing arbitrary (non-executable) information.
- Program accounts (smart contracts) — used (only) for storing executable code.
- Programs can only change the data of data accounts they own.
- Programs can only debit data accounts they own.
- Any program can credit any account.
- Any program can read any account.
- System accounts — accounts that are central to the Solana architecture and perform core operations such as the transfer of lamports, relay of cluster state information, etc.
Types of data accounts:
- System-owned accounts — accounts that are owned (and thus able to be modified) by the System Program (which is a system account).
- PDA (Program Derived Address) accounts — accounts that are owned not by the system program, but by user-built programs.
All accounts have metadata that describe their status. Here are a few fields worth remembering (Rust syntax):
Field | Type | Description |
---|---|---|
key | &’a Pubkey | The key field contains the address to the Solana account. The address is essentially the account’s https://docs.solana.com/terminology#public-key-pubkey. It is in the form of a base58 encoded string. |
is_signer | bool | The is_signer field indicates whether this account has the authority to sign for a given transaction. |
is_writable | bool | The is_writable field indicates whether this account is able to be modified in a given transaction. |
lamports | &’a mut u64 | The lamports field is the amount of https://docs.solana.com/terminology#lamport that is stored in the account. It is a mutable reference to an unsigned 64-bit integer. |
data | &’a mut [u8] | The data field represents the data held in the account that is modifiable by programs. It is stored as a mutable reference to a byte array. |
owner | &’a Pubkey | The owner field is the public key of the program which governs the state transitions of the account. |
executable | bool | The executable field indicates whether this account is a program account (true) or a data account (false). |
rent_epoch | u64 | The rent_epoch field specifies the epoch at which the account will next owe rent. Every account must pay https://docs.solana.com/terminology#rent in order to store its data on the Solana blockchain (more on this later). |
Reading Solana Blockchain State
In order to read account data from the Solana blockchain, we must interact with the Solana JSON RPC API. In this context, an API can be thought of as an intermediary that delivers information to-and-from our client and the Solana blockchain. Essentially, we would make an API call to the Solana JSON RPC API that details the particular information we want, the API would communicate with the network, and finally the API would fetch the information for us (provided that the interaction does not fail).
For example, if we wanted to query the balance of an account with the public key of 9zuSmNxNpH5bDuFPrj4Fc5FPfJAmErzZT3HbMEStm8Ly
, we would use the following TypeScript code:
const getAccountBalance = async (publicKey: PublicKey): Promise<number> => {
const connection = new Connection(ClusterApiUrl('devnet'));
return connection.getBalance(publicKey);
};
getAccountBalance('9zuSmNxNpH5bDuFPrj4Fc5FPfJAmErzZT3HbMEStm8Ly');
In the above example, our client queries the Solana blockchain via the JSON RPC API using the getBalance
method and passes in the address of 9zuSmNxNpH5bDuFPrj4Fc5FPfJAmErzZT3HbMEStm8Ly
as an argument. The number amount of the account’s balance would be fetched and returned.
Altering Solana Blockchain State
Similarly, we can also alter the blockchain’s state by sending transactions via the JSON RPC API. It is important to note that these kind of operations are detailed in instructions which are bundled together and sent to the Solana network as transactions — transactions tell the program(s) what we want to do on the blockchain; if our instructions are valid, then the program(s) performs the given operations.
With reference to the previous example, if we wanted to alter the balance of the account with the address 9zuSmNxNpH5bDuFPrj4Fc5FPfJAmErzZT3HbMEStm8Ly
by sending it some lamports, we would use the following TypeScript code:
const sendSol = async (sender: PublicKey, receiver: PublicKey, amount: number) => {
const instruction = web3.SystemProgram.transfer({
fromPubkey: sender,
lamports: amount * web3.LAMPORTS_PER_SOL,
toPubkey: receiver
});
const transaction = new web3.Transaction().add(instruction);
await sendTransaction(transaction, connection);
};
sendSol(
GfUbb3xe2AZZVGVDKaRzLNiYHSvYLTCwGqR3F94JeDDP, // sender
9zuSmNxNpH5bDuFPrj4Fc5FPfJAmErzZT3HbMEStm8Ly // receiver
);
As mentioned previously, system accounts are responsible for many core operations on the Solana network. In this example, we use the System Program to handle the transfer of lamports from the sender to the receiver. We then wrap this instruction inside of a Transaction()
object and send it via the JSON RPC API using the sendTransaction()
function.
Instructions and Transaction In-Depth
In the above example, we assembled an instruction and a transaction for a native Solana program, the System Program. However, when working with user-built programs, we must pay close attention to detail when designing our instructions and transactions.
Instructions are objects which contain three fields (TS syntax):
Fields | Type | Description |
---|---|---|
keys | Array | The keys field is an array of public keys of the accounts that will be read from or altered in the process of this instruction. |
programId | PublicKey | The programId field is the address of the program we are invoking with this instruction. |
data? | Buffer | The data field is a byte array of any data that we should pass to the given program. The ? indicates that this field may potentially be empty. |
Here is an example of what this would look like in TypeScript:
const connection = new web3.Connection(ClusterApiUrl('devnet'));
const payer = new web3.Keypair.generate();
const instruction = new web3.TransactionInstruction({
// we are interacting with one account
keys: [
{
pubkey: 'GfUbb3xe2AZZVGVDKaRzLNiYHSvYLTCwGqR3F94JeDDP',
isSigner: false,
isWritable: true
},
],
// address of program we're calling
programId: '9zuSmNxNpH5bDuFPrj4Fc5FPfJAmErzZT3HbMEStm8Ly',
// arbitrary data we are feeding to the program as an argument
data: Buffer.from(dataU8Array)
});
const transaction = new web3.Transaction().add(instruction);
await web3.sendAndConfirmTransaction(
connection, // how we connect to the network via the JSON RPC API
transaction, // the operations we want to perform on the network
[payer] // an array of keypairs that will pay for these operations
);
In the above program we:
- Establish a connection to the Solana devnet.
- Create a new keypair that is responsible for paying the gas fee associated with the transaction we want to send to the network.
- Create a new instruction object that contains 1) an array of accounts we want to interact with, 2) the address of the program we want to invoke, and 3) a buffer of arbitrary information we are passing into the program we’re calling.
- Then we envelope our instruction in a transaction object because transaction require instructions.
- Finally, we send our transaction to the network using our connection object, transaction object, and a list of keypairs who will be paying for the transaction.
Project: Creating a Lamports Sender
- Create a folder somewhere on your local environment for the assignment.
- Navigate inside of that folder.
- Run
git clone https://github.com/nathanzebedee/solana-frontend-development.git
inside of your terminal. - Run
cd solana-frontend-development
to enter inside the project folder. - Run
yarn
to install the project dependencies. - Run
yarn dev
to run the project in your local environment. - Navigate to localhost:3000 in your browser to view the project.
- Within the project directory, navigate to pages > sendsol > starter.tsx in order to begin the assignment. Follow the instructions outlined within the document.