Understanding Programs
This page aims to provide a quick overview of how programs work in Solana and offer additional reading material for those who are interested in learning more about a particular subject.
Introductionβ
Unlike most blockchains, Solana separates logic and data into two separate components. These are, respectively, Programs and Accounts. What that means is that instead of storing data inside variables internally, Programs interact with external data stored in Accounts with the ability to mutate them.
This architecture is great for making Programs more modular since the data they interact with is not bound by the Program itself and can scale to new orders of magnitude. It is also great for making Programs more performant since it allows the blockchain to run the same Program in parallel with different Accounts.
To interact with a Program, we must use the Instructions defined by that Program. You can think of Instructions as API endpoints exposed by the Program. Each Instruction contains a set of parameters and constraints that must be fulfilled to execute it.
To recap: In Solana, Programs define Instructions that can be used to interact with external data stores called Accounts.
Note that, technically, Programs are special kinds of Accounts marked as executable
whose entire purpose is to store the compiled code of the Program. However, for the sake of simplicity, we will distinguish these definitions and use the term "Account" to refer to non-executable Accounts.
In the rest of this guide, we will talk about Accounts and Instructions in more detail before explaining the visual representation that we will be using in diagrams throughout the documentation.
π Additional reading:
- Solana Documentation β On-chain Programs
- Solana Cookbook β Programs
- The Anchor Book β Intro to Programming on Solana
Accountsβ
In Solana, Accounts are used to store data. In essence, they are simple arrays of bytes stored at a particular address. The address of an Account is the public key of a cryptographic key pair.
Anyone that has access to the private key of that key pair, can sign on behalf of that Account which, depending on the program, may give them the ability to mutate the data stored in that Account.
Once an Account is created, it is usually immediately initialized by a Program which will be marked as its Owner and will define the data structure of the allocated array of bytes. The Program that owns the Account is then responsible for providing Instructions that can be used to interact with it.
π Additional reading:
Program Derived Addresses (PDA)β
There exist another type of Account, called Program Derived Account, whose address is not the public key of a cryptographic key pair but instead is algorithmically derived from the public key of the Program that owns the Account. We call that address a Program Derived Address or PDA for short.
Since the address is always derived from the public key of the Program, no other Program can algorithmically derive the same address. On top of that, additional Seeds can be provided to the algorithm to add more context to the address.
This has a variety of use cases such as enabling programs to sign Cross-Program Invocations or enabling the creation of accounts within an address that can be derived deterministically.
Note that, by design, Program Derived Addresses will never conflict with cryptographically generated public keys. All cryptographic public keys are part of what we call an Elliptic-curve. If, when generating a PDA, the algorithm generated a key that falls on that curve, a Bump is added to the address and is incremented by one until the generated address no longer falls on the curve.
π Additional reading:
Account dataβ
Whether we are dealing with a regular Account or a Program Derived Account, Accounts store data as a serialized array of bytes. Therefore, it is the responsibility of the Program to define a data structure for each of the Accounts it manages as well as provide a way of differentiating these Accounts, so we know which data structure to apply to them.
Discriminatorsβ
Discriminators are used to differentiate between different types of Accounts within a Program. They can be implemented in many ways but here are the three most common ones:
- Use a shared Enum as the first byte of every account. By prefixing every Account with a shared Enum, we can use the first byte of the serialized data to identify the Account. This is a simple and efficient way to implement discriminators. Most of the programs maintained by Metaplex use this approach.
- Use a deterministic hash as the first byte of every account. This is very similar to the previous point, but it uses a hash instead of an Enum. Programs created using the Anchor framework end up using this approach implicitly because Anchor will automatically generate that hash based on the Account's name.
- No discriminator, use the size of the Account. If all the accounts managed by a Program have different sizes, then we can examine the length of that array of bytes to determine which Account we are dealing with. This is a performant approach since we don't need to add extra bytes to the data, but it limits how flexible a Program can be with its Accounts. The SPL Token Program by Solana uses this approach since it only maintains two accounts of different fixed sizes.
Field types, sizes and offsetsβ
Each Account defines its own data structure by using fields of different types. These types will affect the number of bytes required to store the field. For instance, an i8
is an 8-bit integer that will require 1 byte to store whereas an i64
is a 64-bit integer which will require 8 bytes to store.
Since, in the blockchain, Accounts are just arrays of bytes, it is important to understand the size of each field and where they start in this array, i.e. their offset. This can be useful when fetching multiple accounts from a given program using a memcmp
filter.
Note that not all fields have a fixed size. For instance, a Vec<i8>
is a vector of 8-bit integers that may contain none, one or many items. As such, it becomes a lot more complicated to filter accounts based on fields that are located after the first field of variable size.
Optional fieldsβ
A field can also be defined as optional, meaning there exists a scenario where that field can be empty.
This field will use an additional byte as a prefix to indicate whether the field is empty or not.
Concretely, the value None
will be assigned and the program will act accordingly when using that field.
Indicative fieldsβ
Whilst this is not something that is explicitly defined in the data structure, the documentation will mark certain fields as indicative.
An indicative field means that the information provided by the field is not used by the program itself. Instead, it indicates a piece of information to third parties. The program will still enforce the integrity of the data, but it will simply not use that information internally.
Letβs take the Metadata Account as an example.
The Share
property of each creator in the Creators
array is indicative. The Token Metadata program will ensure that the Share
values of all creators add up to 100%, but it will not do anything with that information. Instead, it expects NFT marketplaces to use this information when distributing royalties.
On the other hand, the Is Mutable
property is not indicative because the Token Metadata program will use that information internally to prevent immutable Metadata Accounts to be updated.
Instructionsβ
One can interact with a Program using the Instructions it provides. Multiple Instructions can be packed into a single Transaction that will be sent to the blockchain. Each Transaction is atomic meaning if any of its instructions fails, the whole Transaction will be reverted.
Similarly to Accounts, Instructions must be serialized into an array of bytes before they can be sent to the network. The data to be serialized must contain the following information for the Program to execute it.
- Discriminator: Similarly to Accounts, Instructions are usually prefixed with a discriminator so the Program can identify which Instruction is being executed.
- Accounts: An array of Account addresses that are affected by this instruction. This can either be because the Account will be read, mutated or both. Note that the order of this array is important since Programs will identify the type of Account provided based on its position.
- Arguments: An array of data fields required by the instruction. It is not uncommon for this array to be empty since Instructions can get most of their information directly from the Accounts. Note that these arguments are comparable to the fields of an Account and, therefore, they can have the same properties mentioned above such as "optional" and "indicative".
- Signers: An array of signatures for a sub-set of the Accounts provided. This is only needed for Accounts that are required to sign the Instruction. The next section explains this in a bit more detail.
π Additional reading:
- Solana Documentation β Transactions
- Solana Documentation β Instructions
- Solana Cookbook β Transactions
Signer and/or Writable Accountsβ
A Program may require that the Accounts provided within an Instruction are Signers and/or Writable.
- Signers: A Signer Account is required to sign the Transaction for the Instruction to be successful. By attaching a signature, users can prove that they are the owner of the Account.
- Writable: A Writable Account will be mutated by the Instruction. This information is important for the blockchain to know which Transactions can be run in parallel and which ones can't.
Therefore, with these two booleans, we end up with the following four possible scenarios:
- Non-Signer and Non-Writable: This Account is only used to read data. We cannot mutate it and we cannot make any assumption about its ownership.
- Signer and Non-Writable: This Account can also not be mutated, but we know that the user who sent the Transaction owns its private key. This enables Programs to grant or deny access to certain actions.
- Signer and Writable: This Account has both signed the Transaction and it can be mutated by the Instruction. This combination is pretty common since Programs will usually require the owner of an Account to prove who they are before mutating that account. Otherwise, anyone could mutate any Account they don't own.
- Non-Signer and Writable: This Account can be mutated, but we can't make any assumption about its ownership. That usually means that the Program is using other Signer Accounts to prove they can mutate that one. This is also the case for PDA Accounts since they are owned by the Program and, as such, they require the Program to keep track of Authorities that can mutate them. Also, note that certain actions like crediting lamports to an Account do not require the Account to sign the Transaction.
Cross-Program Invocations (CPI)β
Cross-Program Invocations allow Programs to execute nested Instructions within their Instructions. They can use Instructions from their own Program and/or from other Programs.
π Additional reading:
Understanding diagramsβ
Each Program's documentation contains its fair share of diagrams to provide a visual representation of how their Accounts are structured and how they are affected by Instructions.
We try our best to provide a consistent visual language that is documented below.
Accountsβ
- Accounts are represented with blue rectangles. Each Account has a smaller grey rectangle below them to indicate which Program owns that Account.
- Accounts belonging to the System Program are represented in purple to differentiate them from other Accounts more easily. These are typically "Wallet Accounts" or Accounts that have not yet been initialized by a Program.
- Underneath the "Owner" rectangle, additional white rectangles can be used to display the data within the Account.
- Optional data attributes in these "Data" rectangles are written in italic.
- In certain situations, we may use an equal sign
=
to show the value assigned to a data attribute. - In certain situations, we may highlight a data attribute in bold to bring more attention to it.
- Relationships between Accounts are represented with solid grey lines.
- The Account that stores the relationship β by keeping track of the other Account's address β has a little triangle at the end of the grey line.
- When Accounts are represented with their data attributes, a dashed grey line is used instead to link the data attribute with the Account it points to.
PDAsβ
- PDA Accounts are represented with brown rectangles and PDA Addresses are displayed as brown rounded rectangles.
- PDA Addresses point to the PDA Account that exists at their address.
- Accounts that are used to derive a PDA Address point towards that PDA Address.
- PDA Addresses sometimes list their seeds next to them. PID refers to the Program ID or public key. Seeds that are wrapped in double quotes are literals.
Note that PDA Addresses can sometimes host different types of Accounts. For instance, the Edition PDA of an NFT can either host a Master Edition Account or an Edition Account based on whether the NFT is the original or not. In this case, PDA Addresses in diagrams can point to more than one PDA Account to show any of these Accounts can live at this address.
Instructionsβ
- Instructions are represented with pink skewed rectangles and point to the Accounts that they change.
- Arrows pointing out of Instructions usually explain in what way the Instruction affects the Account.
- Arrows can also point to data attributes inside Accounts to be slightly more specific.
Showing all Accounts required for an Instruction to be executed in a diagram would make the visual representation too messy and complicated to understand. However, certain diagrams will use arrows from Accounts to Instructions to highlight a subset of the Instruction's required Accounts.