From 349134fee654bd04025f7490c1e17716dd1b1eed Mon Sep 17 00:00:00 2001 From: John <75003086+ZYJLiu@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:36:49 -0600 Subject: [PATCH] Add native rust program basics section (#602) * native rust program basics * . --- docs/programs/anchor/index.md | 2 +- docs/programs/deploying.md | 462 ++++---- docs/programs/examples.md | 2 +- docs/programs/lang-rust.md | 387 ------- docs/programs/rust/index.md | 474 ++++++++ docs/programs/rust/program-structure.md | 1407 +++++++++++++++++++++++ 6 files changed, 2153 insertions(+), 581 deletions(-) delete mode 100644 docs/programs/lang-rust.md create mode 100644 docs/programs/rust/index.md create mode 100644 docs/programs/rust/program-structure.md diff --git a/docs/programs/anchor/index.md b/docs/programs/anchor/index.md index 169fdb33c..2f66ecd0e 100644 --- a/docs/programs/anchor/index.md +++ b/docs/programs/anchor/index.md @@ -5,7 +5,7 @@ description: comprehensive guide covers creating, building, testing, and deploying Solana smart contracts with Anchor. sidebarLabel: Anchor Framework -sidebarSortOrder: 1 +sidebarSortOrder: 0 altRoutes: - /docs/programs/debugging - /docs/programs/lang-c diff --git a/docs/programs/deploying.md b/docs/programs/deploying.md index 394c4de7b..8b82817c5 100644 --- a/docs/programs/deploying.md +++ b/docs/programs/deploying.md @@ -1,25 +1,282 @@ --- title: "Deploying Programs" description: - "Deploying onchain programs can be done using the Solana CLI using the + Deploying onchain programs can be done using the Solana CLI using the Upgradable BPF loader to upload the compiled byte-code to the Solana - blockchain." -sidebarSortOrder: 3 + blockchain. +sidebarSortOrder: 2 --- -Solana onchain programs (otherwise known as "smart contracts") are stored in -"executable" accounts on Solana. These accounts are identical to any other -account but with the exception of: +Solana programs are stored in "executable" accounts on the network. These +accounts contain the program's compiled bytecode that define the instructions +users invoke to interact with the program. -- having the "executable" flag enabled, and -- the owner being assigned to a BPF loader +## CLI Commands -Besides those exceptions, they are governed by the same runtime rules as -non-executable accounts, hold SOL tokens for rent fees, and store a data buffer -which is managed by the BPF loader program. The latest BPF loader is called the -"Upgradeable BPF Loader". +The section is intented as a reference for the basic CLI commands for building +and deploying Solana programs. For a step-by-step guide on creating your first +program, start with [Developing Programs in Rust](/docs/programs/rust). -## Overview of the Upgradeable BPF Loader +### Build Program + +To build your program, use the `cargo build-sbf` command. + +```shell +cargo build-sbf +``` + +This command will: + +1. Compile your program +2. Create a `target/deploy` directory +3. Generate a `.so` file, where `` matches your + program's name in `Cargo.toml` + +The output `.so` file contains your program's compiled bytecode that will be +stored in a Solana account when you deploy your program. + +### Deploy Program + +To deploy your program, use the `solana program deploy` command followed by the +path to the `.so` file created by the `cargo build-sbf` command. + +```shell +solana program deploy ./target/deploy/your_program.so +``` + +During times of congestion, there are a few additional flags you can use to help +with program deployment. + +- `--with-compute-unit-price`: Set compute unit price for transaction, in + increments of 0.000001 lamports (micro-lamports) per compute unit. +- `--max-sign-attempts`: Maximum number of attempts to sign or resign + transactions after blockhash expiration. If any transactions sent during the + program deploy are still unconfirmed after the initially chosen recent + blockhash expires, those transactions will be resigned with a new recent + blockhash and resent. Use this setting to adjust the maximum number of + transaction signing iterations. Each blockhash is valid for about 60 seconds, + which means using the default value of 5 will lead to sending transactions for + at least 5 minutes or until all transactions are confirmed,whichever comes + first. [default: 5] +- `--use-rpc`: Send write transactions to the configured RPC instead of + validator TPUs. This flag requires a stake-weighted RPC connection. + +You can use the flags individually or combine them together. For example: + +```shell +solana program deploy ./target/deploy/your_program.so --with-compute-unit-price 10000 --max-sign-attempts 1000 --use-rpc +``` + +- Use the + [Priority Fee API by Helius](https://docs.helius.dev/guides/priority-fee-api) + to get an estimate of the priority fee to set with the + `--with-compute-unit-price` flag. + +- Get a + [stake-weighted](https://solana.com/developers/guides/advanced/stake-weighted-qos) + RPC connection from [Helius](https://www.helius.dev/) or + [Trition](https://triton.one/) to use with the `--use-rpc` flag. The + `--use-rpc` flag should only be used with a stake-weighted RPC connection. + +To update your default RPC URL with a custom RPC endpoint, use the +`solana config set` command. + +```shell +solana config set --url +``` + +You can view the list of programs you've deployed using the +`solana program show --programs` command. + +```shell +solana program show --programs +``` + +Example output: + +``` +Program Id | Slot | Authority | Balance +2w3sK6CW7Hy1Ljnz2uqPrQsg4KjNZxD4bDerXDkSX3Q1 | 133132 | 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1 | 0.57821592 SOL +``` + +### Update Program + +A program's update authority can modify an existing Solana program by deploying +a new `.so` file to the same program ID. + +To update an existing Solana program: + +- Make changes to your program source code +- Run `cargo build-sbf` to generate an updated `.so` file +- Run `solana program deploy ./target/deploy/your_program.so` to deploy the + updated `.so` file + +The update authority can be changed using the +`solana program set-upgrade-authority` command. + +```shell +solana program set-upgrade-authority --new-upgrade-authority +``` + +### Immutable Program + +A program can be made immutable by removing its update authority. This is an +irreversible action. + +```shell +solana program set-upgrade-authority --final +``` + +You can specify that program should be immutable on deployment by setting the +`--final` flag when deploying the program. + +```shell +solana program deploy ./target/deploy/your_program.so --final +``` + +### Close Program + +You can close your Solana program to reclaim the SOL allocated to the account. +Closing a program is irreversible, so it should be done with caution. To close a +program, use the `solana program close ` command. For example: + +```shell filename="Terminal" +solana program close 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz +--bypass-warning +``` + +Example output: + +``` +Closed Program Id 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz, 0.1350588 SOL +reclaimed +``` + +Note that once a program is closed, its program ID cannot be reused. Attempting +to deploy a program with a previously closed program ID will result in an error. + +``` +Error: Program 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz has been closed, use +a new Program Id +``` + +If you need to redeploy a program after closing it, you must generate a new +program ID. To generate a new keypair for the program, run the following +command: + +```shell filename="Terminal" +solana-keygen new -o ./target/deploy/your_program-keypair.json --force +``` + +Alternatively, you can delete the existing keypair file and run +`cargo build-sbf` again, which will generate a new keypair file. + +### Program Buffer Accounts + +Deploying a program requires multiple transactions due to the 1232 byte limit +for transactions on Solana. An intermediate step of the deploy process involves +writing the program's byte-code to temporary "buffer account". + +This buffer account is automatically closed after successful program deployment. +However, if the deployment fails, the buffer account remains and you can either: + +- Continue the deployment using the existing buffer account +- Close the buffer account to reclaim the allocated SOL (rent) + +You can check if you have any open buffer accounts by using the +`solana program show --buffers` command. + +```shell +solana program show --buffers +``` + +Example output: + +``` +Buffer Address | Authority | Balance +5TRm1DxYcXLbSEbbxWcQbEUCce7L4tVgaC6e2V4G82pM | 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1 | 0.57821592 SOL +``` + +You can continue to the deployment using +`solana program deploy --buffer `. + +For example: + +```shell +solana program deploy --buffer 5TRm1DxYcXLbSEbbxWcQbEUCce7L4tVgaC6e2V4G82pM +``` + +Expected output on successful deployment: + +``` +Program Id: 2w3sK6CW7Hy1Ljnz2uqPrQsg4KjNZxD4bDerXDkSX3Q1 + +Signature: 3fsttJFskUmvbdL5F9y8g43rgNea5tYZeVXbimfx2Up5viJnYehWe3yx45rQJc8Kjkr6nY8D4DP4V2eiSPqvWRNL +``` + +To close buffer accounts, use the `solana program close --buffers` command. + +```shell +solana program close --buffers +``` + +### ELF Dump + +The SBF shared object internals can be dumped to a text file to gain more +insight into a program's composition and what it may be doing at runtime. The +dump will contain both the ELF information as well as a list of all the symbols +and the instructions that implement them. Some of the BPF loader's error log +messages will reference specific instruction numbers where the error occurred. +These references can be looked up in the ELF dump to identify the offending +instruction and its context. + +```shell +cargo build-bpf --dump +``` + +The file will be output to `/target/deploy/your_program-dump.txt`. + +## Program Deployment Process + +Deploying a program on Solana requires multiple transactions, due to the max +size limit of 1232 bytes for Solana transactions. The Solana CLI sends these +transactions with the `solana program deploy` subcommand. The process can be +broken down into the following 3 phases: + +1. [Buffer initialization](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L2113): + First, the CLI sends a transaction which + [creates a buffer account](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L1903) + large enough for the byte-code being deployed. It also invokes the + [initialize buffer instruction](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L320) + to set the buffer authority to restrict writes to the deployer's chosen + address. +2. [Buffer writes](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L2129): + Once the buffer account is initialized, the CLI + [breaks up the program byte-code](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L1940) + into ~1KB chunks and + [sends transactions at a rate of 100 transactions per second](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/client/src/tpu_client.rs#L133) + to write each chunk with + [the write buffer instruction](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L334). + These transactions are sent directly to the current leader's transaction + processing (TPU) port and are processed in parallel with each other. Once all + transactions have been sent, the CLI + [polls the RPC API with batches of transaction signatures](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/client/src/tpu_client.rs#L216) + to ensure that every write was successful and confirmed. +3. [Finalization](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L1807): + Once writes are completed, the CLI + [sends a final transaction](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L2150) + to either + [deploy a new program](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L362) + or + [upgrade an existing program](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L513). + In either case, the byte-code written to the buffer account will be copied + into a program data account and verified. + +## Upgradeable BPF Loader Program + +The BPF loader program is the program that "owns" all executable accounts on +Solana. When you deploy a program, the owner of the program account is set to +the the BPF loader program. ### State accounts @@ -74,182 +331,3 @@ instructions supported by the Upgradeable BPF Loader program: 6. [Close](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/sdk/program/src/loader_upgradeable_instruction.rs#L127): Clears the data of a program data account or buffer account and reclaims the SOL used for the rent exemption deposit. - -## How `solana program deploy` works - -Deploying a program on Solana requires hundreds, if not thousands of -transactions, due to the max size limit of 1232 bytes for Solana transactions. -The Solana CLI takes care of this rapid firing of transactions with the -`solana program deploy` subcommand. The process can be broken down into the -following 3 phases: - -1. [Buffer initialization](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L2113): - First, the CLI sends a transaction which - [creates a buffer account](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L1903) - large enough for the byte-code being deployed. It also invokes the - [initialize buffer instruction](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L320) - to set the buffer authority to restrict writes to the deployer's chosen - address. -2. [Buffer writes](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L2129): - Once the buffer account is initialized, the CLI - [breaks up the program byte-code](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L1940) - into ~1KB chunks and - [sends transactions at a rate of 100 transactions per second](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/client/src/tpu_client.rs#L133) - to write each chunk with - [the write buffer instruction](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L334). - These transactions are sent directly to the current leader's transaction - processing (TPU) port and are processed in parallel with each other. Once all - transactions have been sent, the CLI - [polls the RPC API with batches of transaction signatures](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/client/src/tpu_client.rs#L216) - to ensure that every write was successful and confirmed. -3. [Finalization](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L1807): - Once writes are completed, the CLI - [sends a final transaction](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/cli/src/program.rs#L2150) - to either - [deploy a new program](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L362) - or - [upgrade an existing program](https://github.com/solana-labs/solana/blob/7409d9d2687fba21078a745842c25df805cdf105/programs/bpf_loader/src/lib.rs#L513). - In either case, the byte-code written to the buffer account will be copied - into a program data account and verified. - - - -During times of congestion it is helpful to add priority fees and increase the -max sign attempts. Using a rpc url which has -[stake weighted quality of service](https://solana.com/developers/guides/advanced/stake-weighted-qos) -enabled can also help to make program deploys more reliable. Using Solana -version ^1.18.15 is recommended. - -Example command deploying a program with the Solana CLI: - -```shell -program deploy target/deploy/your_program.so --with-compute-unit-price 10000 --max-sign-attempts 1000 --use-rpc -``` - - - -## Reclaim rent from program accounts - -The storage of data on the Solana blockchain requires the payment of -[rent](/docs/intro/rent.md), including for the byte-code for onchain programs. -Therefore as you deploy more or larger programs, the amount of rent paid to -remain rent-exempt will also become larger. - -Using the current rent cost model configuration, a rent-exempt account requires -a deposit of ~0.7 SOL per 100KB stored. These costs can have an outsized impact -on developers who deploy their own programs since -[program accounts](/docs/core/accounts.md#custom-programs) are among the largest -we typically see on Solana. - -### Reclaiming buffer accounts - -Buffer accounts are used by the Upgradeable BPF loader to temporarily store -byte-code that is in the process of being deployed on-chain. This temporary -buffer is required when upgrading programs because the currently deployed -program's byte-code cannot be affected by an in-progress upgrade. - -Unfortunately, deploys fail occasionally and instead of reusing the buffer -account, developers might retry their deployment with a new buffer and not -realize that they stored a good chunk of SOL in a forgotten buffer account from -an earlier deploy. - -Developers can check if they own any abandoned buffer accounts by using the -Solana CLI: - -```shell -solana program show --buffers --keypair ~/.config/solana/MY_KEYPAIR.json - -Buffer Address | Authority | Balance -9vXW2c3qo6DrLHa1Pkya4Mw2BWZSRYs9aoyoP3g85wCA | 2nr1bHFT86W9tGnyvmYW4vcHKsQB3sVQfnddasz4kExM | 3.41076888 SOL -``` - -And they can close those buffers to reclaim the SOL balance with the following -command: - -```shell -solana program close --buffers --keypair ~/.config/solana/MY_KEYPAIR.json -``` - -#### Fetch the owners of buffer accounts via RPC API - -The owners of all abandoned program deploy buffer accounts can be fetched via -the RPC API: - -```shell -curl http://api.mainnet-beta.solana.com -H "Content-Type: application/json" \ ---data-binary @- << EOF | jq --raw-output '.result | .[] | .account.data[0]' -{ - "jsonrpc":"2.0", "id":1, "method":"getProgramAccounts", - "params":[ - "BPFLoaderUpgradeab1e11111111111111111111111", - { - "dataSlice": {"offset": 5, "length": 32}, - "filters": [{"memcmp": {"offset": 0, "bytes": "2UzHM"}}], - "encoding": "base64" - } - ] -} -EOF -``` - -After re-encoding the base64 encoded keys into base58 and grouping by key, we -see some accounts have over 10 buffer accounts they could close, yikes! - -```shell -'BE3G2F5jKygsSNbPFKHHTxvKpuFXSumASeGweLcei6G3' => 10 buffer accounts -'EsQ179Q8ESroBnnmTDmWEV4rZLkRc3yck32PqMxypE5z' => 10 buffer accounts -'6KXtB89kAgzW7ApFzqhBg5tgnVinzP4NSXVqMAWnXcHs' => 12 buffer accounts -'FinVobfi4tbdMdfN9jhzUuDVqGXfcFnRGX57xHcTWLfW' => 15 buffer accounts -'TESAinbTL2eBLkWqyGA82y1RS6kArHvuYWfkL9dKkbs' => 42 buffer accounts -``` - -### Reclaiming program data accounts - -You may now realize that program data accounts (the accounts that store the -executable byte-code for an on-chain program) can also be closed. - -> **Note:** This does _not_ mean that _program accounts_ can be closed (those -> are immutable and can never be reclaimed, but it's fine they're pretty small). -> It's also important to keep in mind that once program data accounts are -> deleted, they can never be recreated for an existing program. Therefore, the -> corresponding program (and its program id) for any closed program data account -> is effectively disabled forever and may not be re-deployed - -While it would be uncommon for developers to need to close program data accounts -since they can be rewritten during upgrades, one potential scenario is that -since program data accounts can't be _resized_. You may wish to deploy your -program at a new address to accommodate larger executables. - -The ability to reclaim program data account rent deposits also makes testing and -experimentation on the `mainnet-beta` cluster a lot less costly since you could -reclaim everything except the transaction fees and a small amount of rent for -the program account. Lastly, this could help developers recover most of their -funds if they mistakenly deploy a program at an unintended address or on the -wrong cluster. - -To view the programs which are owned by your wallet address, you can run: - -```shell -solana -V # must be 1.7.11 or higher! -solana program show --programs --keypair ~/.config/solana/MY_KEYPAIR.json - -Program Id | Slot | Authority | Balance -CN5x9WEusU6pNH66G22SnspVx4cogWLqMfmb85Z3GW7N | 53796672 | 2nr1bHFT86W9tGnyvmYW4vcHKsQB3sVQfnddasz4kExM | 0.54397272 SOL -``` - -To close those program data accounts and reclaim their SOL balance, you can run: - -```shell -solana program close --programs --keypair ~/.config/solana/MY_KEYPAIR.json -``` - -You might be concerned about this feature allowing malicious actors to close a -program in a way that negatively impacts end users. While this is a valid -concern in general, closing program data accounts doesn't make this any more -exploitable than was already possible. - -Even without the ability to close a program data account, any upgradeable -program could be upgraded to a no-op implementation and then have its upgrade -authority cleared to make it immutable forever. This new feature for closing -program data accounts merely adds the ability to reclaim the rent deposit, -disabling a program was already technically possible. diff --git a/docs/programs/examples.md b/docs/programs/examples.md index 8e829521e..d16eaf27a 100644 --- a/docs/programs/examples.md +++ b/docs/programs/examples.md @@ -23,7 +23,7 @@ keywords: - blockchain tutorial - web3 developer - anchor -sidebarSortOrder: 2 +sidebarSortOrder: 3 --- The diff --git a/docs/programs/lang-rust.md b/docs/programs/lang-rust.md deleted file mode 100644 index 503ceaccb..000000000 --- a/docs/programs/lang-rust.md +++ /dev/null @@ -1,387 +0,0 @@ ---- -title: "Developing with Rust" -sidebarSortOrder: 4 ---- - -Solana supports writing onchain programs using the -[Rust](https://www.rust-lang.org/) programming language. - -- [Setup your local environment](/docs/intro/installation) and use the local - test validator. - -## Project Layout - -Solana Rust programs follow the typical -[Rust project layout](https://doc.rust-lang.org/cargo/guide/project-layout.html): - -```text -/inc/ -/src/ -/Cargo.toml -``` - -Solana Rust programs may depend directly on each other in order to gain access -to instruction helpers when making -[cross-program invocations](/docs/core/cpi.md). When doing so it's important to -not pull in the dependent program's entrypoint symbols because they may conflict -with the program's own. To avoid this, programs should define an `no-entrypoint` -feature in `Cargo.toml` and use to exclude the entrypoint. - -- [Define the feature](https://github.com/solana-labs/solana-program-library/blob/fca9836a2c8e18fc7e3595287484e9acd60a8f64/token/program/Cargo.toml#L12) -- [Exclude the entrypoint](https://github.com/solana-labs/solana-program-library/blob/fca9836a2c8e18fc7e3595287484e9acd60a8f64/token/program/src/lib.rs#L12) - -Then when other programs include this program as a dependency, they should do so -using the `no-entrypoint` feature. - -- [Include without entrypoint](https://github.com/solana-labs/solana-program-library/blob/fca9836a2c8e18fc7e3595287484e9acd60a8f64/token-swap/program/Cargo.toml#L22) - -## Project Dependencies - -At a minimum, Solana Rust programs must pull in the -[`solana-program`](https://crates.io/crates/solana-program) crate. - -Solana SBF programs have some [restrictions](#restrictions) that may prevent the -inclusion of some crates as dependencies or require special handling. - -For example: - -- Crates that require the architecture be a subset of the ones supported by the - official toolchain. There is no workaround for this unless that crate is - forked and SBF added to that those architecture checks. -- Crates may depend on `rand` which is not supported in Solana's deterministic - program environment. To include a `rand` dependent crate refer to - [Depending on Rand](#depending-on-rand). -- Crates may overflow the stack even if the stack overflowing code isn't - included in the program itself. For more information refer to - [Stack](/docs/programs/faq.md#stack). - -## How to Build - -First setup the environment: - -- Install the latest Rust stable from https://rustup.rs/ -- Install the latest [Solana command-line tools](/docs/intro/installation.md) - -The normal cargo build is available for building programs against your host -machine which can be used for unit testing: - -```shell -cargo build -``` - -To build a specific program, such as SPL Token, for the Solana SBF target which -can be deployed to the cluster: - -```shell -cd -cargo build-bpf -``` - -## How to Test - -Solana programs can be unit tested via the traditional `cargo test` mechanism by -exercising program functions directly. - -To help facilitate testing in an environment that more closely matches a live -cluster, developers can use the -[`program-test`](https://crates.io/crates/solana-program-test) crate. The -`program-test` crate starts up a local instance of the runtime and allows tests -to send multiple transactions while keeping state for the duration of the test. - -For more information the -[test in sysvar example](https://github.com/solana-labs/solana-program-library/blob/master/examples/rust/sysvar/tests/functional.rs) -shows how an instruction containing sysvar account is sent and processed by the -program. - -## Program Entrypoint - -Programs export a known entrypoint symbol which the Solana runtime looks up and -calls when invoking a program. Solana supports multiple versions of the BPF -loader and the entrypoints may vary between them. Programs must be written for -and deployed to the same loader. For more details see the -[FAQ section on Loaders](/docs/programs/faq.md#loaders). - -Currently there are two supported loaders -[BPF Loader](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/bpf_loader.rs#L17) -and -[BPF loader deprecated](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/bpf_loader_deprecated.rs#L14) - -They both have the same raw entrypoint definition, the following is the raw -symbol that the runtime looks up and calls: - -```rust -#[no_mangle] -pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64; -``` - -This entrypoint takes a generic byte array which contains the serialized program -parameters (program id, accounts, instruction data, etc...). To deserialize the -parameters each loader contains its own wrapper macro that exports the raw -entrypoint, deserializes the parameters, calls a user defined instruction -processing function, and returns the results. - -You can find the entrypoint macros here: - -- [BPF Loader's entrypoint macro](https://github.com/solana-labs/solana/blob/9b1199cdb1b391b00d510ed7fc4866bdf6ee4eb3/sdk/program/src/entrypoint.rs#L42) -- [BPF Loader deprecated's entrypoint macro](https://github.com/solana-labs/solana/blob/9b1199cdb1b391b00d510ed7fc4866bdf6ee4eb3/sdk/program/src/entrypoint_deprecated.rs#L38) - -The program defined instruction processing function that the entrypoint macros -call must be of this form: - -```rust -pub type ProcessInstruction = - fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult; -``` - -### Parameter Deserialization - -Each loader provides a helper function that deserializes the program's input -parameters into Rust types. The entrypoint macros automatically calls the -deserialization helper: - -- [BPF Loader deserialization](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/entrypoint.rs#L146) -- [BPF Loader deprecated deserialization](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/entrypoint_deprecated.rs#L57) - -Some programs may want to perform deserialization themselves and they can by -providing their own implementation of the [raw entrypoint](#program-entrypoint). -Take note that the provided deserialization functions retain references back to -the serialized byte array for variables that the program is allowed to modify -(lamports, account data). The reason for this is that upon return the loader -will read those modifications so they may be committed. If a program implements -their own deserialization function they need to ensure that any modifications -the program wishes to commit be written back into the input byte array. - -Details on how the loader serializes the program inputs can be found in the -[Input Parameter Serialization](/docs/programs/faq.md#input-parameter-serialization) -docs. - -### Data Types - -The loader's entrypoint macros call the program defined instruction processor -function with the following parameters: - -```rust -program_id: &Pubkey, -accounts: &[AccountInfo], -instruction_data: &[u8] -``` - -The program id is the public key of the currently executing program. - -The accounts is an ordered slice of the accounts referenced by the instruction -and represented as an -[AccountInfo](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/account_info.rs#L12) -structures. An account's place in the array signifies its meaning, for example, -when transferring lamports an instruction may define the first account as the -source and the second as the destination. - -The members of the `AccountInfo` structure are read-only except for `lamports` -and `data`. Both may be modified by the program in accordance with the "runtime -enforcement policy". Both of these members are protected by the Rust `RefCell` -construct, so they must be borrowed to read or write to them. The reason for -this is they both point back to the original input byte array, but there may be -multiple entries in the accounts slice that point to the same account. Using -`RefCell` ensures that the program does not accidentally perform overlapping -read/writes to the same underlying data via multiple `AccountInfo` structures. -If a program implements their own deserialization function care should be taken -to handle duplicate accounts appropriately. - -The instruction data is the general purpose byte array from the -[instruction's instruction data](/docs/core/transactions.md#instruction) being -processed. - -## Heap - -Rust programs implement the heap directly by defining a custom -[`global_allocator`](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/entrypoint.rs#L72) - -Programs may implement their own `global_allocator` based on its specific needs. -Refer to the [custom heap example](#examples) for more information. - -## Restrictions - -On-chain Rust programs support most of Rust's libstd, libcore, and liballoc, as -well as many 3rd party crates. - -There are some limitations since these programs run in a resource-constrained, -single-threaded environment, as well as being deterministic: - -- No access to - - `rand` - - `std::fs` - - `std::net` - - `std::future` - - `std::process` - - `std::sync` - - `std::task` - - `std::thread` - - `std::time` -- Limited access to: - - `std::hash` - - `std::os` -- Bincode is extremely computationally expensive in both cycles and call depth - and should be avoided -- String formatting should be avoided since it is also computationally - expensive. -- No support for `println!`, `print!`, the Solana [logging helpers](#logging) - should be used instead. -- The runtime enforces a limit on the number of instructions a program can - execute during the processing of one instruction. See - [computation budget](/docs/core/fees.md#compute-budget) for more information. - -## Depending on Rand - -Programs are constrained to run deterministically, so random numbers are not -available. Sometimes a program may depend on a crate that depends itself on -`rand` even if the program does not use any of the random number functionality. -If a program depends on `rand`, the compilation will fail because there is no -`get-random` support for Solana. The error will typically look like this: - -```shell -error: target is not supported, for more information see: https://docs.rs/getrandom/#unsupported-targets - --> /Users/jack/.cargo/registry/src/github.com-1ecc6299db9ec823/getrandom-0.1.14/src/lib.rs:257:9 - | -257 | / compile_error!("\ -258 | | target is not supported, for more information see: \ -259 | | https://docs.rs/getrandom/#unsupported-targets\ -260 | | "); - | |___________^ -``` - -To work around this dependency issue, add the following dependency to the -program's `Cargo.toml`: - -```rust -getrandom = { version = "0.1.14", features = ["dummy"] } -``` - -or if the dependency is on getrandom v0.2 add: - -```rust -getrandom = { version = "0.2.2", features = ["custom"] } -``` - -## Logging - -Rust's `println!` macro is computationally expensive and not supported. Instead -the helper macro -[`msg!`](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/log.rs#L33) -is provided. - -`msg!` has two forms: - -```rust -msg!("A string"); -``` - -or - -```rust -msg!(0_64, 1_64, 2_64, 3_64, 4_64); -``` - -Both forms output the results to the program logs. If a program so wishes they -can emulate `println!` by using `format!`: - -```rust -msg!("Some variable: {:?}", variable); -``` - -## Panicking - -Rust's `panic!`, `assert!`, and internal panic results are printed to the -program logs by default. - -```shell -INFO solana_runtime::message_processor] Finalized account CGLhHSuWsp1gT4B7MY2KACqp9RUwQRhcUFfVSuxpSajZ -INFO solana_runtime::message_processor] Call SBF program CGLhHSuWsp1gT4B7MY2KACqp9RUwQRhcUFfVSuxpSajZ -INFO solana_runtime::message_processor] Program log: Panicked at: 'assertion failed: `(left == right)` - left: `1`, - right: `2`', rust/panic/src/lib.rs:22:5 -INFO solana_runtime::message_processor] SBF program consumed 5453 of 200000 units -INFO solana_runtime::message_processor] SBF program CGLhHSuWsp1gT4B7MY2KACqp9RUwQRhcUFfVSuxpSajZ failed: BPF program panicked -``` - -### Custom Panic Handler - -Programs can override the default panic handler by providing their own -implementation. - -First define the `custom-panic` feature in the program's `Cargo.toml` - -```rust -[features] -default = ["custom-panic"] -custom-panic = [] -``` - -Then provide a custom implementation of the panic handler: - -```rust -#[cfg(all(feature = "custom-panic", target_os = "solana"))] -#[no_mangle] -fn custom_panic(info: &core::panic::PanicInfo<'_>) { - solana_program::msg!("program custom panic enabled"); - solana_program::msg!("{}", info); -} -``` - -In the above snippit, the default implementation is shown, but developers may -replace that with something that better suits their needs. - -One of the side effects of supporting full panic messages by default is that -programs incur the cost of pulling in more of Rust's `libstd` implementation -into program's shared object. Typical programs will already be pulling in a fair -amount of `libstd` and may not notice much of an increase in the shared object -size. But programs that explicitly attempt to be very small by avoiding `libstd` -may take a significant impact (~25kb). To eliminate that impact, programs can -provide their own custom panic handler with an empty implementation. - -```rust -#[cfg(all(feature = "custom-panic", target_os = "solana"))] -#[no_mangle] -fn custom_panic(info: &core::panic::PanicInfo<'_>) { - // Do nothing to save space -} -``` - -## Compute Budget - -Use the system call `sol_remaining_compute_units()` to return a `u64` indicating -the number of compute units remaining for this transaction. - -Use the system call -[`sol_log_compute_units()`](https://github.com/solana-labs/solana/blob/d9b0fc0e3eec67dfe4a97d9298b15969b2804fab/sdk/program/src/log.rs#L141) -to log a message containing the remaining number of compute units the program -may consume before execution is halted - -See the [Compute Budget](/docs/core/fees.md#compute-budget) documentation for -more information. - -## ELF Dump - -The SBF shared object internals can be dumped to a text file to gain more -insight into a program's composition and what it may be doing at runtime. The -dump will contain both the ELF information as well as a list of all the symbols -and the instructions that implement them. Some of the BPF loader's error log -messages will reference specific instruction numbers where the error occurred. -These references can be looked up in the ELF dump to identify the offending -instruction and its context. - -To create a dump file: - -```shell -cd -cargo build-bpf --dump -``` - -## Examples - -The -[Solana Program Library GitHub](https://github.com/solana-labs/solana-program-library/tree/master/examples/rust) -repo contains a collection of Rust examples. - -The -[Solana Developers Program Examples GitHub](https://github.com/solana-developers/program-examples) -repo also contains a collection of beginner to intermediate Rust program -examples. diff --git a/docs/programs/rust/index.md b/docs/programs/rust/index.md new file mode 100644 index 000000000..7fe456337 --- /dev/null +++ b/docs/programs/rust/index.md @@ -0,0 +1,474 @@ +--- +title: Developing Programs in Rust +description: + Learn how to develop Solana programs using Rust, including step-by-step + instructions for creating, building, testing, and deploying smart contracts on + the Solana blockchain. +sidebarLabel: Rust Programs +sidebarSortOrder: 1 +altRoutes: + - /docs/programs/lang-rust +--- + +Solana programs are primarily developed using the Rust programming language. +This page focuses on writing Solana programs in Rust without using the Anchor +framework, an approach often referred to as writing "native Rust" programs. + +Native Rust development provides developers with direct control over their +Solana programs. However, this approach requires more manual setup and +boilerplate code compared to using the Anchor framework. This method is +recommended for developers who: + +- Seek granular control over program logic and optimizations +- Want to learn the underlying concepts before moving to higher-level frameworks + +For beginners, we recommend starting with the Anchor framework. See the +[Anchor](/docs/programs/anchor) section for more information. + +## Prerequisites + +For detailed installation instructions, visit the +[installation](/docs/intro/installation) page. + +Before you begin, ensure you have the following installed: + +- Rust: The programming language for building Solana programs. +- Solana CLI: Command-line tool for Solana development. + +## Getting Started + +The example below covers the basic steps to create your first Solana program +written in Rust. We'll create a minimal program that prints "Hello, world!" to +the program log. + + + +### Create a new Program + +First, create a new Rust project using the standard `cargo init` command with +the `--lib` flag. + +```shell filename="Terminal" +cargo init hello_world --lib +``` + +Navigate to the project directory. You should see the default `src/lib.rs` and +`Cargo.toml` files + +```shell filename="Terminal" +cd hello_world +``` + +Next, add the `solana-program` dependency. This is the minimum dependency +required to build a Solana program. + +```shell filename="Terminal" +cargo add solana-program@1.18.26 +``` + +Next, add the following snippet to `Cargo.toml`. If you don't include this +config, the `target/deploy` directory will not be generated when you build the +program. + +```toml filename="Cargo.toml" +[lib] +crate-type = ["cdylib", "lib"] +``` + +Your `Cargo.toml` file should look like the following: + +```toml filename="Cargo.toml" +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +solana-program = "1.18.26" +``` + +Next, replace the contents of `src/lib.rs` with the following code. This is a +minimal Solana program that prints "Hello, world!" to the program log when the +program is invoked. + +The `msg!` macro is used in Solana programs to print a message to the program +log. + +```rs filename="lib.rs" +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); + +pub fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + msg!("Hello, world!"); + Ok(()) +} +``` + +### Build the Program + +Next, build the program using the `cargo build-sbf` command. + +```shell filename="Terminal" +cargo build-sbf +``` + +This command generates a `target/deploy` directory containing two important +files: + +1. A `.so` file (e.g., `hello_world.so`): This is the compiled Solana program + that will be deployed to the network as a "smart contract". +2. A keypair file (e.g., `hello_world-keypair.json`): The public key of this + keypair is used as the program ID when deploying the program. + +To view the program ID, run the following command in your terminal. This command +prints the public key of the keypair at the specified file path: + +```shell filename="Terminal" +solana address -k ./target/deploy/hello_world-keypair.json +``` + +Example output: + +``` +4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz +``` + +### Test the Program + +Next, test the program using the `solana-program-test` crate. Add the following +dependencies to `Cargo.toml`. + +```shell filename="Terminal" +cargo add solana-program-test@1.18.26 --dev +cargo add solana-sdk@1.18.26 --dev +cargo add tokio --dev +``` + +Add the following test to `src/lib.rs`, below the program code. This is a test +module that invokes the hello world program. + +```rs filename="lib.rs" +#[cfg(test)] +mod test { + use super::*; + use solana_program_test::*; + use solana_sdk::{signature::Signer, transaction::Transaction}; + + #[tokio::test] + async fn test_hello_world() { + let program_id = Pubkey::new_unique(); + let (mut banks_client, payer, recent_blockhash) = + ProgramTest::new("hello_world", program_id, processor!(process_instruction)) + .start() + .await; + + // Create the instruction to invoke the program + let instruction = + solana_program::instruction::Instruction::new_with_borsh(program_id, &(), vec![]); + + // Add the instruction to a new transaction + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + // Process the transaction + let transaction_result = banks_client.process_transaction(transaction).await; + assert!(transaction_result.is_ok()); + } +} +``` + +Run the test using the `cargo test-sbf` command. The program log will display +"Hello, world!". + +```shell filename="Terminal" +cargo test-sbf +``` + +Example output: + +```shell filename="Terminal" {4} /Program log: Hello, world!/ +running 1 test +[2024-10-18T21:24:54.889570000Z INFO solana_program_test] "hello_world" SBF program from /hello_world/target/deploy/hello_world.so, modified 35 seconds, 828 ms, 268 µs and 398 ns ago +[2024-10-18T21:24:54.974294000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1] +[2024-10-18T21:24:54.974814000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, world! +[2024-10-18T21:24:54.976848000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 140 of 200000 compute units +[2024-10-18T21:24:54.976868000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success +test test::test_hello_world ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s +``` + +### Deploy the Program + +Next, deploy the program. When developing locally, we can use the +`solana-test-validator`. + +First, configure the Solana CLI to use the local Solana cluster. + +```shell filename="Terminal" +solana config set -ul +``` + +Example output: + +``` +Config File: /.config/solana/cli/config.yml +RPC URL: http://localhost:8899 +WebSocket URL: ws://localhost:8900/ (computed) +Keypair Path: /.config/solana/id.json +Commitment: confirmed +``` + +Open a new terminal and run the `solana-test-validators` command to start the +local validator. + +```shell filename="Terminal" +solana-test-validator +``` + +While the test validator is running, run the `solana program deploy` command in +a separate terminal to deploy the program to the local validator. + +```shell filename="Terminal" +solana program deploy ./target/deploy/hello_world.so +``` + +Example output: + +``` +Program Id: 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz +Signature: +5osMiNMiDZGM7L1e2tPHxU8wdB8gwG8fDnXLg5G7SbhwFz4dHshYgAijk4wSQL5cXiu8z1MMou5kLadAQuHp7ybH +``` + +You can inspect the program ID and transaction signature on +[Solana Explorer](https://explorer.solana.com/?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899). +Note that the cluster on Solana Explorer must also be localhost. The "Custom RPC +URL" option on Solana Explorer defaults to `http://localhost:8899`. + +### Invoke the Program + +Next, we'll demonstrate how to invoke the program using a Rust client. + +First create an `examples` directory and a `client.rs` file. + +```shell filename="Terminal" +mkdir -p examples +touch examples/client.rs +``` + +Add the following to `Cargo.toml`. + +```toml filename="Cargo.toml" +[[example]] +name = "client" +path = "examples/client.rs" +``` + +Add the `solana-client` dependency. + +```shell filename="Terminal" +cargo add solana-client@1.18.26 --dev +``` + +Add the following code to `examples/client.rs`. This is a Rust client script +that funds a new keypair to pay for transaction fees and then invokes the hello +world program. + +```rs filename="example/client.rs" +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + commitment_config::CommitmentConfig, + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use std::str::FromStr; + +#[tokio::main] +async fn main() { + // Program ID (replace with your actual program ID) + let program_id = Pubkey::from_str("4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz").unwrap(); + + // Connect to the Solana devnet + let rpc_url = String::from("http://127.0.0.1:8899"); + let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed()); + + // Generate a new keypair for the payer + let payer = Keypair::new(); + + // Request airdrop + let airdrop_amount = 1_000_000_000; // 1 SOL + let signature = client + .request_airdrop(&payer.pubkey(), airdrop_amount) + .expect("Failed to request airdrop"); + + // Wait for airdrop confirmation + loop { + let confirmed = client.confirm_transaction(&signature).unwrap(); + if confirmed { + break; + } + } + + // Create the instruction + let instruction = Instruction::new_with_borsh( + program_id, + &(), // Empty instruction data + vec![], // No accounts needed + ); + + // Add the instruction to new transaction + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], client.get_latest_blockhash().unwrap()); + + // Send and confirm the transaction + match client.send_and_confirm_transaction(&transaction) { + Ok(signature) => println!("Transaction Signature: {}", signature), + Err(err) => eprintln!("Error sending transaction: {}", err), + } +} +``` + +Before running the script, replace the program ID in the code snippet above with +the one for your program. + +You can get your program ID by running the following command. + +```shell filename="Terminal" +solana address -k ./target/deploy/hello_world-keypair.json +``` + +```diff +#[tokio::main] +async fn main() { +- let program_id = Pubkey::from_str("4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz").unwrap(); ++ let program_id = Pubkey::from_str("YOUR_PROGRAM_ID).unwrap(); + } +} +``` + +Run the client script with the following command. + +```shell filename="Terminal" +cargo run --example client +``` + +Example output: + +``` +Transaction Signature: 54TWxKi3Jsi3UTeZbhLGUFX6JQH7TspRJjRRFZ8NFnwG5BXM9udxiX77bAACjKAS9fGnVeEazrXL4SfKrW7xZFYV +``` + +You can inspect the transaction signature on +[Solana Explorer](https://explorer.solana.com/?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899) +(local cluster) to see "Hello, world!" in the program log. + +### Update the Program + +Solana programs can be updated by redeploying to the same program ID. Update the +program in `src/lib.rs` to print "Hello, Solana!" instead of "Hello, world!". + +```diff filename="lib.rs" +pub fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { +- msg!("Hello, world!"); ++ msg!("Hello, Solana!"); + Ok(()) +} +``` + +Test the updated program by running the `cargo test-sbf` command. + +```shell filename="Terminal" +cargo test-sbf +``` + +You should see "Hello, Solana!" in the program log. + +```shell filename="Terminal" {4} +running 1 test +[2024-10-23T19:28:28.842639000Z INFO solana_program_test] "hello_world" SBF program from /code/misc/delete/hello_world/target/deploy/hello_world.so, modified 4 minutes, 31 seconds, 435 ms, 566 µs and 766 ns ago +[2024-10-23T19:28:28.934854000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1] +[2024-10-23T19:28:28.936735000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, Solana! +[2024-10-23T19:28:28.938774000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 140 of 200000 compute units +[2024-10-23T19:28:28.938793000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success +test test::test_hello_world ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s +``` + +Run the `cargo build-sbf` command to generate an updated `.so` file. + +```shell filename="Terminal" +cargo build-sbf +``` + +Redeploy the program using the `solana program deploy` command. + +```shell filename="Terminal" +solana program deploy ./target/deploy/hello_world.so +``` + +Run the client code again and inspect the transaction signature on Solana +Explorer to see "Hello, Solana!" in the program log. + +```shell filename="Terminal" +cargo run --example client +``` + +### Close the Program + +You can close your Solana program to reclaim the SOL allocated to the account. +Closing a program is irreversible, so it should be done with caution. + +To close a program, use the `solana program close ` command. For +example: + +```shell filename="Terminal" +solana program close 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz +--bypass-warning +``` + +Example output: + +``` +Closed Program Id 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz, 0.1350588 SOL +reclaimed +``` + +Note that once a program is closed, its program ID cannot be reused. Attempting +to deploy a program with a previously closed program ID will result in an error. + +``` +Error: Program 4Ujf5fXfLx2PAwRqcECCLtgDxHKPznoJpa43jUBxFfMz has been closed, use +a new Program Id +``` + +If you need to redeploy a program with the same source code after closing a +program, you must generate a new program ID. To generate a new keypair for the +program, run the following command: + +```shell filename="Terminal" +solana-keygen new -o ./target/deploy/hello_world-keypair.json --force +``` + +Alternatively, you can delete the existing keypair file (e.g. +`./target/deploy/hello_world-keypair.json`) and run `cargo build-sbf` again, +which will generate a new keypair file. + + diff --git a/docs/programs/rust/program-structure.md b/docs/programs/rust/program-structure.md new file mode 100644 index 000000000..b07022736 --- /dev/null +++ b/docs/programs/rust/program-structure.md @@ -0,0 +1,1407 @@ +--- +title: Rust Program Structure +sidebarLabel: Program Structure +description: + Learn how to structure Solana programs in Rust, including entrypoints, state + management, instruction handling, and testing. +sidebarSortOrder: 1 +--- + +Solana programs written in Rust have minimal structural requirements, allowing +for flexibility in how code is organized. The only requirement is that a program +must have an `entrypoint`, which defines where the execution of a program +begins. + +## Program Structure + +While there are no strict rules for file structure, Solana programs typically +follow a common pattern: + +- `entrypoint.rs`: Defines the entrypoint that routes incoming instructions. +- `state.rs`: Define program-specific state (account data). +- `instructions.rs`: Defines the instructions that the program can execute. +- `processor.rs`: Defines the instruction handlers (functions) that implement + the business logic for each instruction. +- `error.rs`: Defines custom errors that the program can return. + +You can find examples in the +[Solana Program Library](https://github.com/solana-labs/solana-program-library/tree/master/token/program/src). + +## Example Program + +To demonstrate how to build a native Rust program with multiple instructions, +we'll walk through a simple counter program that implements two instructions: + +1. `InitializeCounter`: Creates and initializes a new account with an initial + value. +2. `IncrementCounter`: Increments the value stored in an existing account. + +For simplicity, the program will be implemented in a single `lib.rs` file, +though in practice you may want to split larger programs into multiple files. + + + + +```rs filename="lib.rs" +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint, + entrypoint::ProgramResult, + msg, + program::invoke, + program_error::ProgramError, + pubkey::Pubkey, + system_instruction, + sysvar::{rent::Rent, Sysvar}, +}; + +// Program entrypoint +entrypoint!(process_instruction); + +// Function to route instructions to the correct handler +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // Unpack instruction data + let instruction = CounterInstruction::unpack(instruction_data)?; + + // Match instruction type + match instruction { + CounterInstruction::InitializeCounter { initial_value } => { + process_initialize_counter(program_id, accounts, initial_value)? + } + CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?, + }; + Ok(()) +} + +// Instructions that our program can execute +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum CounterInstruction { + InitializeCounter { initial_value: u64 }, // variant 0 + IncrementCounter, // variant 1 +} + +impl CounterInstruction { + pub fn unpack(input: &[u8]) -> Result { + // Get the instruction variant from the first byte + let (&variant, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + // Match instruction type and parse the remaining bytes based on the variant + match variant { + 0 => { + // For InitializeCounter, parse a u64 from the remaining bytes + let initial_value = u64::from_le_bytes( + rest.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + Ok(Self::InitializeCounter { initial_value }) + } + 1 => Ok(Self::IncrementCounter), // No additional data needed + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +// Initialize a new counter account +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let counter_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + // Size of our counter account + let account_space = 8; // Size in bytes to store a u64 + + // Calculate minimum balance for rent exemption + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(account_space); + + // Create the counter account + invoke( + &system_instruction::create_account( + payer_account.key, // Account paying for the new account + counter_account.key, // Account to be created + required_lamports, // Amount of lamports to transfer to the new account + account_space as u64, // Size in bytes to allocate for the data field + program_id, // Set program owner to our program + ), + &[ + payer_account.clone(), + counter_account.clone(), + system_program.clone(), + ], + )?; + + // Create a new CounterAccount struct with the initial value + let counter_data = CounterAccount { + count: initial_value, + }; + + // Get a mutable reference to the counter account's data + let mut account_data = &mut counter_account.data.borrow_mut()[..]; + + // Serialize the CounterAccount struct into the account's data + counter_data.serialize(&mut account_data)?; + + msg!("Counter initialized with value: {}", initial_value); + + Ok(()) +} + +// Update an existing counter's value +fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let counter_account = next_account_info(accounts_iter)?; + + // Verify account ownership + if counter_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Mutable borrow the account data + let mut data = counter_account.data.borrow_mut(); + + // Deserialize the account data into our CounterAccount struct + let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?; + + // Increment the counter value + counter_data.count = counter_data + .count + .checked_add(1) + .ok_or(ProgramError::InvalidAccountData)?; + + // Serialize the updated counter data back into the account + counter_data.serialize(&mut &mut data[..])?; + + msg!("Counter incremented to: {}", counter_data.count); + Ok(()) +} + +// Struct representing our counter account's data +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CounterAccount { + count: u64, +} + +#[cfg(test)] +mod test { + use super::*; + use solana_program_test::*; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signer}, + system_program, + transaction::Transaction, + }; + + #[tokio::test] + async fn test_counter_program() { + let program_id = Pubkey::new_unique(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "counter_program", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + // Create a new keypair to use as the address for our counter account + let counter_keypair = Keypair::new(); + let initial_value: u64 = 42; + + // Step 1: Initialize the counter + println!("Testing counter initialization..."); + + // Create initialization instruction + let mut init_instruction_data = vec![0]; // 0 = initialize instruction + init_instruction_data.extend_from_slice(&initial_value.to_le_bytes()); + + let initialize_instruction = Instruction::new_with_bytes( + program_id, + &init_instruction_data, + vec![ + AccountMeta::new(counter_keypair.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + // Send transaction with initialize instruction + let mut transaction = + Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 42); + println!( + "✅ Counter initialized successfully with value: {}", + counter.count + ); + } + + // Step 2: Increment the counter + println!("Testing counter increment..."); + + // Create increment instruction + let increment_instruction = Instruction::new_with_bytes( + program_id, + &[1], // 1 = increment instruction + vec![AccountMeta::new(counter_keypair.pubkey(), true)], + ); + + // Send transaction with increment instruction + let mut transaction = + Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 43); + println!("✅ Counter incremented successfully to: {}", counter.count); + } + } +} +``` + +```toml filename="Cargo.toml" +[package] +name = "counter_program" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +borsh = "1.5.1" +solana-program = "1.18.26" + +[dev-dependencies] +solana-program-test = "1.18.26" +solana-sdk = "1.18.26" +tokio = "1.41.0" +``` + + + + + + +### Create a new Program + +First, create a new Rust project using the standard `cargo init` command with +the `--lib` flag. + +```shell filename="Terminal" +cargo init counter_program --lib +``` + +Navigate to the project directory. You should see the default `src/lib.rs` and +`Cargo.toml` files + +```shell filename="Terminal" +cd counter_program +``` + +Next, add the `solana-program` dependency. This is the minimum dependency +required to build a Solana program. + +```shell filename="Terminal" +cargo add solana-program@1.18.26 +``` + +Next, add the following snippet to `Cargo.toml`. If you don't include this +config, the `target/deploy` directory will not be generated when you build the +program. + +```toml filename="Cargo.toml" +[lib] +crate-type = ["cdylib", "lib"] +``` + +Your `Cargo.toml` file should look like the following: + +```toml filename="Cargo.toml" +[package] +name = "counter_program" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +solana-program = "1.18.26" +``` + +### Program Entrypoint + +A Solana program entrypoint is the function that gets called when a program is +invoked. The entrypoint has the following raw definition and developers are free +to create their own implementation of the entrypoint function. + +For simplicity, use the +[`entrypoint!`](https://github.com/solana-labs/solana/blob/v2.0/sdk/program/src/entrypoint.rs#L124-L140) +macro from the `solana_program` crate to define the entrypoint in your program. + +```rs +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64; +``` + +Replace the default code in `lib.rs` with the following code. This snippet: + +1. Imports the required dependencies from `solana_program` +2. Defines the program entrypoint using the `entrypoint!` macro +3. Implements the `process_instruction` function that will route instructions to + the appropriate handler functions + +```rs filename="lib.rs" {13} /process_instruction/ +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint, + entrypoint::ProgramResult, + msg, + program::invoke, + program_error::ProgramError, + pubkey::Pubkey, + system_instruction, + sysvar::{rent::Rent, Sysvar}, +}; + +entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // Your program logic + Ok(()) +} +``` + +The `entrypoint!` macro requires a function with the the following +[type signature](https://github.com/solana-labs/solana/blob/v2.0/sdk/program/src/entrypoint.rs#L28-L29) +as an argument: + +```rs +pub type ProcessInstruction = + fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult; +``` + +When a Solana program is invoked, the entrypoint +[deserializes](https://github.com/solana-labs/solana/blob/v2.0/sdk/program/src/entrypoint.rs#L277) +the +[input data](https://github.com/solana-labs/solana/blob/v2.0/sdk/program/src/entrypoint.rs#L129-L131) +(provided as bytes) into three values and passes them to the +[`process_instruction`](https://github.com/solana-labs/solana/blob/v2.0/sdk/program/src/entrypoint.rs#L132) +function: + +- `program_id`: The public key of the program being invoked (current program) +- `accounts`: The `AccountInfo` for accounts required by the instruction being + invoked +- `instruction_data`: Additional data passed to the program which specifies the + instruction to execute and its required arguments + +These three parameters directly correspond to the data that clients must provide +when building an instruction to invoke a program. + +### Define Program State + +When building a Solana program, you'll typically start by defining your +program's state - the data that will be stored in accounts created and owned by +your program. + +Program state is defined using Rust structs that represent the data layout of +your program's accounts. You can define multiple structs to represent different +types of accounts for your program. + +When working with accounts, you need a way to convert your program's data types +to and from the raw bytes stored in an account's data field: + +- Serialization: Converting your data types into bytes to store in an account's + data field +- Deserialization: Converting the bytes stored in an account back into your data + types + +While you can use any serialization format for Solana program development, +[Borsh](https://borsh.io/) is commonly used. To use Borsh in your Solana +program: + +1. Add the `borsh` crate as a dependency to your `Cargo.toml`: + +```shell filename="Terminal" +cargo add borsh +``` + +2. Import the Borsh traits and use the derive macro to implement the traits for + your structs: + +```rust +use borsh::{BorshSerialize, BorshDeserialize}; + +// Define struct representing our counter account's data +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CounterAccount { + count: u64, +} +``` + +Add the `CounterAccount` struct to `lib.rs` to define the program state. This +struct will be used in both the initialization and increment instructions. + +```rs filename="lib.rs" {12} {25-29} +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint, + entrypoint::ProgramResult, + msg, + program::invoke, + program_error::ProgramError, + pubkey::Pubkey, + system_instruction, + sysvar::{rent::Rent, Sysvar}, +}; +use borsh::{BorshSerialize, BorshDeserialize}; + +entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // Your program logic + Ok(()) +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CounterAccount { + count: u64, +} +``` + +### Define Instructions + +Instructions refer to the different operations that your Solana program can +perform. Think of them as public APIs for your program - they define what +actions users can take when interacting with your program. + +Instructions are typically defined using a Rust enum where: + +- Each enum variant represents a different instruction +- The variant's payload represents the instruction's parameters + +Note that Rust enum variants are implicitly numbered starting from 0. + +Below is an example of an enum defining two instructions: + +```rust +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum CounterInstruction { + InitializeCounter { initial_value: u64 }, // variant 0 + IncrementCounter, // variant 1 +} +``` + +When a client invokes your program, they must provide instruction data (as a +buffer of bytes) where: + +- The first byte identifies which instruction variant to execute (0, 1, etc.) +- The remaining bytes contain the serialized instruction parameters (if + required) + +To convert the instruction data (bytes) into a variant of the enum, it is common +to implement a helper method. This method: + +1. Splits the first byte to get the instruction variant +2. Matches on the variant and parses any additional parameters from the + remaining bytes +3. Returns the corresponding enum variant + +For example, the `unpack` method for the `CounterInstruction` enum: + +```rust +impl CounterInstruction { + pub fn unpack(input: &[u8]) -> Result { + // Get the instruction variant from the first byte + let (&variant, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + // Match instruction type and parse the remaining bytes based on the variant + match variant { + 0 => { + // For InitializeCounter, parse a u64 from the remaining bytes + let initial_value = u64::from_le_bytes( + rest.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)? + ); + Ok(Self::InitializeCounter { initial_value }) + } + 1 => Ok(Self::IncrementCounter), // No additional data needed + _ => Err(ProgramError::InvalidInstructionData), + } + } +} +``` + +Add the following code to `lib.rs` to define the instructions for the counter +program. + +```rs filename="lib.rs" {18-46} +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, + program_error::ProgramError, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // Your program logic + Ok(()) +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub enum CounterInstruction { + InitializeCounter { initial_value: u64 }, // variant 0 + IncrementCounter, // variant 1 +} + +impl CounterInstruction { + pub fn unpack(input: &[u8]) -> Result { + // Get the instruction variant from the first byte + let (&variant, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + // Match instruction type and parse the remaining bytes based on the variant + match variant { + 0 => { + // For InitializeCounter, parse a u64 from the remaining bytes + let initial_value = u64::from_le_bytes( + rest.try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ); + Ok(Self::InitializeCounter { initial_value }) + } + 1 => Ok(Self::IncrementCounter), // No additional data needed + _ => Err(ProgramError::InvalidInstructionData), + } + } +} +``` + +### Instruction Handlers + +Instruction handlers refer to the functions that contain the business logic for +each instruction. It's common to name handler functions as +`process_`, but you're free to choose any naming convention. + +Add the following code to `lib.rs`. This code uses the `CounterInstruction` enum +and `unpack` method defined in the previous step to route incoming instructions +to the appropriate handler functions: + +```rs filename="lib.rs" {8-17} {20-32} /process_initialize_counter/1 /process_increment_counter/1 +entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // Unpack instruction data + let instruction = CounterInstruction::unpack(instruction_data)?; + + // Match instruction type + match instruction { + CounterInstruction::InitializeCounter { initial_value } => { + process_initialize_counter(program_id, accounts, initial_value)? + } + CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?, + }; +} + +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + // Implementation details... + Ok(()) +} + +fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + // Implementation details... + Ok(()) +} +``` + +Next, add the implementation of the `process_initialize_counter` function. This +instruction handler: + +1. Creates and allocates space for a new account to store the counter data +2. Initializing the account data with `initial_value` passed to the instruction + + + + +The `process_initialize_counter` function requires three accounts: + +1. The counter account that will be created and initialized +2. The payer account that will fund the new account creation +3. The System Program that we invoke to create the new account + +To define the accounts required by the instruction, we create an iterator over +the `accounts` slice and use the `next_account_info` function to get each +account. The number of accounts you define are the accounts required by the +instruction. + +The order of accounts is important - when building the instruction on the client +side, accounts must be provided in the same order as it is defined in the +program for the instruction to execute successfully. + +While the variable names for the accounts have no effect on the program's +functionality, using descriptive names is recommended. + +```rs filename="lib.rs" {6-10} +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let counter_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + Ok(()) +} +``` + +Before creating an account, we need to: + +1. Specify the space (in bytes) to allocate to the account's data field. Since + we're storing a u64 value (`count`), we need 8 bytes. + +2. Calculate the minimum "rent" balance required. On Solana, accounts must + maintain a minimum balance of lamports (rent) based on amount of data stored + on the account. + +```rs filename="lib.rs" {12-17} +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let counter_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + // Size of our counter account + let account_space = 8; // Size in bytes to store a u64 + + // Calculate minimum balance for rent exemption + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(account_space); + + Ok(()) +} +``` + +Once the space is defined and rent is calculated, create the account by invoking +the System Program's `create_account` instruction. + +On Solana, new accounts can only be created by the System Program. When creating +an account, we specify the amount of bytes to allocate and the program owner of +the new account. The System Program: + +1. Creates the new account +2. Allocates the specified space for the account's data field +3. Transfers ownership to the specified program + +This ownership transfer is important because only the program owner of an +account can modify an account's data. In this case, we set our program as the +owner, which will allow us to modify the account's data to store the counter +value. + +To invoke the System Program from our program's instruction, we make a Cross +Program Invocation (CPI) via the `invoke` function. A CPI allows one program to +call instructions on other programs - in this case, the System Program's +`create_account` instruction. + +```rs filename="lib.rs" {19-33} +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let counter_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + // Size of our counter account + let account_space = 8; // Size in bytes to store a u64 + + // Calculate minimum balance for rent exemption + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(account_space); + + // Create the counter account + invoke( + &system_instruction::create_account( + payer_account.key, // Account paying for the new account + counter_account.key, // Account to be created + required_lamports, // Amount of lamports to transfer to the new account + account_space as u64, // Size in bytes to allocate for the data field + program_id, // Set program owner to our program + ), + &[ + payer_account.clone(), + counter_account.clone(), + system_program.clone(), + ], + )?; + + Ok(()) +} +``` + +Once the account is created, we initialize the account data by: + +1. Creating a new `CounterAccount` struct with the `initial_value` provided to + the instruction. +2. Getting a mutable reference to the new account's data field. +3. Serializing the `CounterAccount` struct into the account's data field, + effectively storing the `initial_value` on the account. + +```rs filename="lib.rs" {35-44} /inital_value/ +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let counter_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + // Size of our counter account + let account_space = 8; // Size in bytes to store a u64 + + // Calculate minimum balance for rent exemption + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(account_space); + + // Create the counter account + invoke( + &system_instruction::create_account( + payer_account.key, // Account paying for the new account + counter_account.key, // Account to be created + required_lamports, // Amount of lamports to transfer to the new account + account_space as u64, // Size in bytes to allocate for the data field + program_id, // Set program owner to our program + ), + &[ + payer_account.clone(), + counter_account.clone(), + system_program.clone(), + ], + )?; + + // Create a new CounterAccount struct with the initial value + let counter_data = CounterAccount { + count: initial_value, + }; + + // Get a mutable reference to the counter account's data + let mut account_data = &mut counter_account.data.borrow_mut()[..]; + + // Serialize the CounterAccount struct into the account's data + counter_data.serialize(&mut account_data)?; + + msg!("Counter initialized with value: {}", initial_value); + + Ok(()) +} +``` + + + + +```rs filename="lib.rs" +// Initialize a new counter account +fn process_initialize_counter( + program_id: &Pubkey, + accounts: &[AccountInfo], + initial_value: u64, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let counter_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + // Size of our counter account + let account_space = 8; // Size in bytes to store a u64 + + // Calculate minimum balance for rent exemption + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(account_space); + + // Create the counter account + invoke( + &system_instruction::create_account( + payer_account.key, // Account paying for the new account + counter_account.key, // Account to be created + required_lamports, // Amount of lamports to transfer to the new account + account_space as u64, // Size in bytes to allocate for the data field + program_id, // Set program owner to our program + ), + &[ + payer_account.clone(), + counter_account.clone(), + system_program.clone(), + ], + )?; + + // Create a new CounterAccount struct with the initial value + let counter_data = CounterAccount { + count: initial_value, + }; + + // Get a mutable reference to the counter account's data + let mut account_data = &mut counter_account.data.borrow_mut()[..]; + + // Serialize the CounterAccount struct into the account's data + counter_data.serialize(&mut account_data)?; + + msg!("Counter initialized with value: {}", initial_value); + + Ok(()) +} +``` + +Next, add the implementation of the `process_increment_counter` function. This +instruction increments the value of an existing counter account. + + + + +Just like the `process_initialize_counter` function, we start by creating an +iterator over the accounts. In this case, we are only expecting one account, +which is the account to be updated. + +Note that in practice, a developer must implement various security checks to +validate the accounts passed to the program. Since all accounts are provided by +the caller of the instruction, there is no guarantee that the accounts provided +are the ones the program expects. Missing account validation checks are a common +source of program vulnerabilities. + +The example below includes a check to ensure the account we're referring to as +the `counter_account` is owned by the executing program. + +```rs filename="lib.rs" {6-9} +// Update an existing counter's value +fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let counter_account = next_account_info(accounts_iter)?; + + // Verify account ownership + if counter_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + Ok(()) +} +``` + +To update the account data, we: + +- Mutably borrow the existing account's data field +- Deserialize the raw bytes into our `CounterAccount` struct +- Update the `count` value +- Serialize the modified struct back into the account's data field + +```rs filename="lib.rs" {11-24} +// Update an existing counter's value +fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let counter_account = next_account_info(accounts_iter)?; + + // Verify account ownership + if counter_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Mutable borrow the account data + let mut data = counter_account.data.borrow_mut(); + + // Deserialize the account data into our CounterAccount struct + let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?; + + // Increment the counter value + counter_data.count = counter_data + .count + .checked_add(1) + .ok_or(ProgramError::InvalidAccountData)?; + + // Serialize the updated counter data back into the account + counter_data.serialize(&mut &mut data[..])?; + + msg!("Counter incremented to: {}", counter_data.count); + Ok(()) +} +``` + + + + +```rs filename="lib.rs" +// Update an existing counter's value +fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let counter_account = next_account_info(accounts_iter)?; + + // Verify account ownership + if counter_account.owner != program_id { + return Err(ProgramError::IncorrectProgramId); + } + + // Mutable borrow the account data + let mut data = counter_account.data.borrow_mut(); + + // Deserialize the account data into our CounterAccount struct + let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?; + + // Increment the counter value + counter_data.count = counter_data + .count + .checked_add(1) + .ok_or(ProgramError::InvalidAccountData)?; + + // Serialize the updated counter data back into the account + counter_data.serialize(&mut &mut data[..])?; + + msg!("Counter incremented to: {}", counter_data.count); + Ok(()) +} +``` + +### Instruction Testing + +To test the program instructions, add the following dependencies to +`Cargo.toml`. + +```shell filename="Terminal" +cargo add solana-program-test@1.18.26 --dev +cargo add solana-sdk@1.18.26 --dev +cargo add tokio --dev +``` + +Then add the following test module to `lib.rs` and run `cargo test-sbf` to +execute the tests. Optionally, use the `--nocapture` flag to see the print +statements in the output. + +```shell filename="Terminal" +cargo test-sbf -- --nocapture +``` + + + + +First, set up the test module and import required dependencies: + +```rs filename="lib.rs" +#[cfg(test)] +mod test { + use super::*; + use solana_program_test::*; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signer}, + system_program, + transaction::Transaction, + }; + + #[tokio::test] + async fn test_counter_program() { + // Test code will go here + } +} +``` + +Next, set up the test using `ProgramTest`. Then create a new keypair to use as +the address for the counter account we'll initialize and define an initial value +to set for the counter. + +```rs filename="lib.rs" +#[cfg(test)] +mod test { + use super::*; + use solana_program_test::*; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signer}, + system_program, + transaction::Transaction, + }; + + #[tokio::test] + async fn test_counter_program() { + let program_id = Pubkey::new_unique(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "counter_program", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + // Create a new keypair to use as the address for our counter account + let counter_keypair = Keypair::new(); + let initial_value: u64 = 42; + } +} +``` + +When building an instruction, each account must be provided as an +[`AccountMeta`](https://github.com/solana-labs/solana/blob/v2.0/sdk/program/src/instruction.rs#L539-L545), +which specifies: + +- The account's public key (`Pubkey`) +- `is_writable`: Whether the account data will be modified +- `is_signer`: Whether the account must sign the transaction + +```rs +AccountMeta::new(account1_pubkey, true), // writable, signer +AccountMeta::new(account2_pubkey, false), // writable, not signer +AccountMeta::new_readonly(account3_pubkey, false), // not writable, not signer +AccountMeta::new_readonly(account4_pubkey, true), // writable, signer +``` + +To test the initialize instruction: + +- Create instruction data with variant 0 (`InitializeCounter`) and initial value +- Build the instruction with the program ID, instruction data, and required + accounts +- Send a transaction with the initialize instruction +- Check the account was created with the correct initial value + +```rs filename="lib.rs" {16-53} + #[tokio::test] + async fn test_counter_program() { + let program_id = Pubkey::new_unique(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "counter_program", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + // Create a new keypair to use as the address for our counter account + let counter_keypair = Keypair::new(); + let initial_value: u64 = 42; + + // Step 1: Initialize the counter + println!("Testing counter initialization..."); + + // Create initialization instruction + let mut init_instruction_data = vec![0]; // 0 = initialize instruction + init_instruction_data.extend_from_slice(&initial_value.to_le_bytes()); + + let initialize_instruction = Instruction::new_with_bytes( + program_id, + &init_instruction_data, + vec![ + AccountMeta::new(counter_keypair.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + // Send transaction with initialize instruction + let mut transaction = + Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 42); + println!( + "✅ Counter initialized successfully with value: {}", + counter.count + ); + } + } +``` + +To test the increment instruction: + +- Build the instruction with the program ID, instruction data, and required + accounts +- Send a transaction with the increment instruction +- Check the account was incremented to the correct value + +Note that the instruction data for the increment instruction is `[1]`, which +corresponds to variant 1 (`IncrementCounter`). Since there are no additional +parameters to the increment instruction, the data is simply the instruction +variant. + +```rs filename="lib.rs" {55-82} + #[tokio::test] + async fn test_counter_program() { + let program_id = Pubkey::new_unique(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "counter_program", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + // Create a new keypair to use as the address for our counter account + let counter_keypair = Keypair::new(); + let initial_value: u64 = 42; + + // Step 1: Initialize the counter + println!("Testing counter initialization..."); + + // Create initialization instruction + let mut init_instruction_data = vec![0]; // 0 = initialize instruction + init_instruction_data.extend_from_slice(&initial_value.to_le_bytes()); + + let initialize_instruction = Instruction::new_with_bytes( + program_id, + &init_instruction_data, + vec![ + AccountMeta::new(counter_keypair.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + // Send transaction with initialize instruction + let mut transaction = + Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 42); + println!( + "✅ Counter initialized successfully with value: {}", + counter.count + ); + } + + // Step 2: Increment the counter + println!("Testing counter increment..."); + + // Create increment instruction + let increment_instruction = Instruction::new_with_bytes( + program_id, + &[1], // 1 = increment instruction + vec![AccountMeta::new(counter_keypair.pubkey(), true)], + ); + + // Send transaction with increment instruction + let mut transaction = + Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 43); + println!("✅ Counter incremented successfully to: {}", counter.count); + } + } +``` + + + + +```rs filename="lib.rs" +#[cfg(test)] +mod test { + use super::*; + use solana_program_test::*; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signer}, + system_program, + transaction::Transaction, + }; + + #[tokio::test] + async fn test_counter_program() { + let program_id = Pubkey::new_unique(); + let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( + "counter_program", + program_id, + processor!(process_instruction), + ) + .start() + .await; + + // Create a new keypair to use as the address for our counter account + let counter_keypair = Keypair::new(); + let initial_value: u64 = 42; + + // Step 1: Initialize the counter + println!("Testing counter initialization..."); + + // Create initialization instruction + let mut init_instruction_data = vec![0]; // 0 = initialize instruction + init_instruction_data.extend_from_slice(&initial_value.to_le_bytes()); + + let initialize_instruction = Instruction::new_with_bytes( + program_id, + &init_instruction_data, + vec![ + AccountMeta::new(counter_keypair.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + // Send transaction with initialize instruction + let mut transaction = + Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 42); + println!( + "✅ Counter initialized successfully with value: {}", + counter.count + ); + } + + // Step 2: Increment the counter + println!("Testing counter increment..."); + + // Create increment instruction + let increment_instruction = Instruction::new_with_bytes( + program_id, + &[1], // 1 = increment instruction + vec![AccountMeta::new(counter_keypair.pubkey(), true)], + ); + + // Send transaction with increment instruction + let mut transaction = + Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &counter_keypair], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + + // Check account data + let account = banks_client + .get_account(counter_keypair.pubkey()) + .await + .expect("Failed to get counter account"); + + if let Some(account_data) = account { + let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data) + .expect("Failed to deserialize counter data"); + assert_eq!(counter.count, 43); + println!("✅ Counter incremented successfully to: {}", counter.count); + } + } +} +``` + +Example output: + +```shell filename="Terminal" {6} {10} +running 1 test +[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago +[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1] +[2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2] +[2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success +[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42 +[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units +[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success +[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1] +[2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43 +[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units +[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success +test test::test_counter_program ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s +``` + +