Skip to content

Generate short-lived unique program derive address signers.

License

Notifications You must be signed in to change notification settings

nifty-oss/limestone

Repository files navigation

limestone

Limestone

Generate short-lived unique program derive address signers.

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 to program_id if omitted).

  • slot: The slot number for the derivation (the slot needs to be within the valid range, i.e., not older than current slot - TTL).

Important

create_account uses the default TTL value of 150 slots. This is typically the number of slots that a blockhash is available and maximizes the chance of the account creation to succeed. You can use the create_account_with_ttl if you want to use a different TTL value – a lower TTL provides a shorter interval for the PDA signer to be available. At the same time, if your transaction is not executed within the TTL slots, 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 limestone program uses a default of 150 slots as the TTL value.

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 when it would do so. By limiting when this happens, we limit when a particular account can be created.

In Limestone, PDAs are derived using a slot number and each slot value has a time-to-leave (TTL) associated with it. The TTL is used to validate whether the slot used to derive the PDA is too old or not. When the slot is deemed too old, Limestone will not sign the instruction — i.e., PDAs have a slot "range" (slot > current slot - TTL) defining when they can be used as signers.

This in practice restricts the ability to recreate the same account: if the account is closed after TTL slots have passed, there is no way to recreate the same account. This mitigates concerns of closeable mints, for example. A mint account can be created using an address generated by Limestone and be safely closeable without worries that it can be recreated in the future — there is no need to impose restrictions that a mint cannot be closed. This is particularly interesting for non-fungible protocols to prevent an NFT being recreated after it is burned — currently there are NFT standards that do not close the NFT account on burn to avoid account recreation, preventing users to fully recover rent funds and keeping unnecessary account state.

The advantage of using a slot value is that there is no write contention since Limestone does not require a "global" account. An alternative way to achieved something similar is to use a program with a sequential counter stored on an account, where the counter is incremented every time a new account is created to provide an unique value. The drawback of this approach is that there is a write contention on the account storing the counter, which makes it more difficult for different clients to use it concurrently.

Limitations

Although the use TTL defines a time period where the account creation is allowed — 150 slots is approximately 1 minute 19 seconds assuming 400ms block times — it does not guarantee that the account is not closed and recreated between that interval. Additionally, it does not prevent an account being created, closed and recreated on the same transaction.

For protocols that need such guarantee, an addional restriction should be added when closing an account that should not be recreated. The protocol should store the slot value used on the account derivation and validate that the account is being closed after slot + TTL — this will prevent the account recreation since the slot value will be too old to generate a PDA signer.

Since a slot value is part of the derivation of the account, it cannot be easily used in scenarios where durable nonces are required to build transactions. It is very likely that the slot value will be invalid when the transaction is signed at a point in the future. This limitation is not due to the approach of using a PDA signer — it arises from the fact that the slot expires in the same way that a blockhash expires. The alternative in this case is to use an approach where the slot in the derivation is replaced by the nonce value, which will provide a similar guarantee that a derivation is only valid for a particular nonce value.

License

Copyright (c) 2024 nifty-oss maintainers

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.