Limestone
Generate short-lived unique program derive address signers.
Install / Use
/learn @nifty-oss/LimestoneREADME
Overview
Limestone enables the creation short-lived program derived addresses (PDAs) signers. These signers are used to create accounts which can be "safely" closed since the same account address (PDA signer) cannot be recreated after a time period, measured in terms of slots.
This feature is useful to avoid reusing an account for something completely different or in cases when applications or indexers store any information about the account, which could get out of sync if the account is closed and recreated on the same address.
You can use Limestone as a library or invoke its instruction — either directly from a client or through a cross program invocation — in your project. In both cases, you delegate the account creation to Limestone. The only difference is the program that signs for the PDA: when used as a library, your program is the signer; the Limestone program is the signer when its instruction is used.
[!IMPORTANT] While PDA and PDA accounts are usually used interchangeably, a PDA is an address and not necessarily an account. More importantly, a PDA can be used to create an account owned by a different program than the one used to derive the PDA — one of the main uses of this is to allow programs to be signers.
Using it as a library
From your project folder:
cargo add limestone
On your program, you replace the use of system_instruction::create_account with limestone::create_account:
use limestone::{Arguments, create_account};
create_account(
program_id,
Arguments {
to: ctx.accounts.to,
from: ctx.accounts.from,
lamports,
space,
owner: Some(system_program::ID),
slot,
},
)?;
The arguments for the create_account are as follows:
-
program_id: It is the address of your program (the account derivation will be done within the scope of the program). -
from(signer, writable): It is the funding account. -
to(writable): It is the account to be created (must be a PDA of[from, slot]derived from program_id). -
lamports: The lamports to be transferred to the new account (must be at least the amount needed for the account to be rent-exempt). -
space: The data size for the new account. -
owner: Optinal program that will own the new account (it default toprogram_idif omitted). -
slot: The slot number for the derivation (the slot needs to be within the valid range, i.e., not older thancurrent slot - TTL).
[!IMPORTANT]
create_accountuses the defaultTTLvalue of150slots. This is typically the number of slots that ablockhashis available and maximizes the chance of the account creation to succeed. You can use thecreate_account_with_ttlif you want to use a differentTTLvalue – a lowerTTLprovides a shorter interval for the PDA signer to be available. At the same time, if your transaction is not executed within theTTLslots, it will fail.
Using the limestone program
Limestone has a deployed program that can be used directly either from a client or another program and a companion client library with instruction builders. There are JavaScript and Rust client packages.
JavaScript
Install the library using the package manager of your choice:
npm install @nifty-oss/limestone
The package contains an instruction builder:
const slot = await client.rpc.getSlot().send();
const createAccountIx = await getCreateAccountInstructionAsync({
from: payer,
lamports: 500_000_000n,
owner: address('AssetGtQBTSgm5s91d1RAQod5JmaZiJDxqsgtqrZud73'),
space: 200,
slot,
});
[!NOTE] The package uses the new Solana JavaScript SDK. There is also a package using the Metaplex Umi framework.
Rust
From your project folder:
cargo add limestone-client
The CreateAccountBuilder builds the necessary instruction to create and account:
use limestone_client::{find_pda, instructions::CreateAccountBuilder};
let (pda, _) = find_pda(&payer.pubkey(), slot);
let create_ix = CreateAccountBuilder::new()
.from(payer.pubkey())
.to(pda)
.lamports(5_000_000_000)
.space(200)
.owner(system_program::ID)
.slot(slot)
.instruction();
The same arguments used for the create_account function are used in the instruction builder.
When used in a program, the CreateAccountCpiBuilder can be used directly to invoke the create_account instruction:
use limestone_client::instructions::CreateAccountCpiBuilder;
CreateAccountCpiBuilder::new(program_info)
.from(&payer_info)
.to(&pda_info)
.system_program(&system_program_info)
.lamports(5_000_000_000)
.space(200)
.owner(system_program::ID)
.slot(slot)
.invoke()?;
[!IMPORTANT] The
limestoneprogram uses a default of150slots as theTTLvalue.
How it works
Limestone takes adavantage of how PDAs are handled in the runtime — a program can sign an instruction on behalf of PDAs derived from its program ID. This provides an important property: there is no private key generated for the address and, since the program is the only one that can sign on behalf of the PDA, there is an opportunity to control w
