179 lines
6.5 KiB
Markdown
179 lines
6.5 KiB
Markdown
# `@solana/spl-account-compression`
|
|
|
|
A TypeScript library for interacting with SPL Account Compression and SPL NoOp. [Docs here](https://solana-labs.github.io/solana-program-library/account-compression/sdk/docs/).
|
|
|
|
## Install
|
|
|
|
```shell
|
|
npm install --save @solana/spl-account-compression @solana/web3.js
|
|
```
|
|
|
|
__OR__
|
|
|
|
```shell
|
|
yarn add @solana/spl-account-compression @solana/web3.js
|
|
```
|
|
|
|
## Information
|
|
|
|
This on-chain program provides an interface for composing smart contracts to create and use SPL ConcurrentMerkleTrees.
|
|
The primary application of using SPL ConcurrentMerkleTrees is to synchronize off-chain databases with on-chain updates.
|
|
|
|
SPL ConcurrentMerkleTrees are Merkle Trees that have their roots on-chain with support for fast-forwarding proofs. Fast forwarding allows multiple updates to the tree in a single block and reduces the latency burden on indexers.
|
|
|
|
|
|
In order to execute transactions that modify an SPL ConcurrentMerkleTree, an indexer will need to
|
|
parse through transactions that touch the tree in order to provide up-to-date merkle proofs.
|
|
For more information regarding merkle proofs, see this great [explainer](https://ethereum.org/en/developers/tutorials/merkle-proofs-for-offline-data-integrity/).
|
|
|
|
This program is targeted towards supporting [Metaplex Compressed NFTs](https://github.com/metaplex-foundation/metaplex-program-library/tree/master/bubblegum) and may be subject to change.
|
|
|
|
A **rough draft** of the whitepaper for SPL ConcurrentMerkleTrees can be found [here](https://drive.google.com/file/d/1BOpa5OFmara50fTvL0VIVYjtg-qzHCVc/view).
|
|
|
|
## High Level Overview
|
|
|
|
### Instructions
|
|
Code to interact with the on-chain instructions is auto-generated by `@metaplex-foundation/solita`.
|
|
Exported functions to create instructions have pattern `create<instructionName>Instruction`.
|
|
* For example, account compression's `append_leaf` instruction has a `Solita`-generated factory function called
|
|
`createAppendLeafInstruction`.
|
|
|
|
`Solita` provides very low-level functions to create instructions. Thus, helper functions are provided for each instruction, denoted with the suffix `ix`.
|
|
* For example: `createReplaceLeafInstruction` has a helper function `createReplaceLeafIx`
|
|
|
|
### Modules
|
|
|
|
A merkle tree reference implementation is provided to index the on-chain trees. The `MerkleTree` class and its helpers are provided
|
|
under `src/merkle-tree`.
|
|
|
|
The `MerkleTree` class is meant to follow a similar interface as `MerkleTree` from [`merkletreejs`](https://www.npmjs.com/package/merkletreejs).
|
|
|
|
| Feature | Our Tree | `merkletreejs` | Notes |
|
|
| ---------- | -------- | -------------- | ------------------------------------------------------------ |
|
|
| updateLeaf | ✅ | ❌ | This is the unique feature of `ConcurrentMerkleTree`'s |
|
|
| multiProof | ❌ | ✅ | Possible to support in future version of Account Compression |
|
|
| addLeaf | ✅ | ✅ | Our version does this via `updateLeaf()` |
|
|
|
|
If you'd like to see more features added, please create an issue with the title `Account Compression` and your feature request.
|
|
|
|
### Examples
|
|
|
|
1. Create a tree
|
|
|
|
```typescript
|
|
// Assume: known `payer` Keypair
|
|
|
|
// Generate a keypair for the ConcurrentMerkleTree
|
|
const cmtKeypair = Keypair.generate();
|
|
|
|
// Create a system instruction to allocate enough
|
|
// space for the tree
|
|
const allocAccountIx = await createAllocTreeIx(
|
|
connection,
|
|
cmtKeypair.publicKey,
|
|
payer.publicKey,
|
|
{ maxDepth, maxBufferSize },
|
|
canopyDepth,
|
|
);
|
|
|
|
// Create an SPL compression instruction to initialize
|
|
// the newly created ConcurrentMerkleTree
|
|
const initTreeIx = createInitEmptyMerkleTreeIx(
|
|
cmtKeypair.publicKey,
|
|
payer.publicKey,
|
|
{ maxDepth, maxBufferSize }
|
|
);
|
|
|
|
const tx = new Transaction().add(allocAccountIx).add(initTreeIx);
|
|
|
|
await sendAndConfirmTransaction(connection, tx, [cmtKeypair, payer]);
|
|
```
|
|
|
|
2. Add a leaf to the tree
|
|
|
|
```typescript
|
|
// Create a new leaf
|
|
const newLeaf: Buffer = crypto.randomBytes(32);
|
|
|
|
// Add the new leaf to the existing tree
|
|
const appendIx = createAppendIx(cmtKeypair.publicKey, payer.publicKey, newLeaf);
|
|
|
|
const tx = new Transaction().add(appendIx);
|
|
|
|
await sendAndConfirmTransaction(connection, tx, [payer]);
|
|
```
|
|
|
|
3. Replace a leaf in the tree, using the provided `MerkleTree` as an indexer
|
|
|
|
This example assumes that `offChainTree` has been indexing all previous modifying transactions
|
|
involving this tree.
|
|
It is okay for the indexer to be behind by a maximum of `maxBufferSize` transactions.
|
|
|
|
|
|
```typescript
|
|
// Assume: `offChainTree` is a MerkleTree instance
|
|
// that has been indexing the `cmtKeypair.publicKey` transactions
|
|
|
|
// Get a new leaf
|
|
const newLeaf: Buffer = crypto.randomBytes(32);
|
|
|
|
// Query off-chain records for information about the leaf
|
|
// you wish to replace by its index in the tree
|
|
const leafIndex = 314;
|
|
|
|
// Replace the leaf at `leafIndex` with `newLeaf`
|
|
const replaceIx = createReplaceIx(
|
|
cmtKeypair.publicKey,
|
|
payer.publicKey,
|
|
newLeaf,
|
|
offChainTree.getProof(leafIndex)
|
|
);
|
|
|
|
const tx = new Transaction().add(replaceIx);
|
|
|
|
await sendAndConfirmTransaction(connection, tx, [payer]);
|
|
```
|
|
|
|
4. Replace a leaf in the tree, using a 3rd party indexer
|
|
|
|
This example assumes that some 3rd party service is indexing the the tree at `cmtKeypair.publicKey` for you, and providing MerkleProofs via some REST endpoint.
|
|
The `getProofFromAnIndexer` function is a **placeholder** to exemplify this relationship.
|
|
|
|
```typescript
|
|
// Get a new leaf
|
|
const newLeaf: Buffer = crypto.randomBytes(32);
|
|
|
|
// Query off-chain indexer for a MerkleProof
|
|
// possibly by executing GET request against a REST api
|
|
const proof = await getProofFromAnIndexer(myOldLeaf);
|
|
|
|
// Replace `myOldLeaf` with `newLeaf` at the same index in the tree
|
|
const replaceIx = createReplaceIx(
|
|
cmtKeypair.publicKey,
|
|
payer.publicKey,
|
|
newLeaf,
|
|
proof
|
|
);
|
|
|
|
const tx = new Transaction().add(replaceIx);
|
|
|
|
await sendAndConfirmTransaction(connection, tx, [payer]);
|
|
```
|
|
|
|
## Reference examples
|
|
|
|
Here are some examples using account compression in the wild:
|
|
|
|
* Solana Program Library [tests](https://github.com/solana-labs/solana-program-library/tree/master/account-compression/sdk/tests)
|
|
|
|
* Metaplex Program Library Compressed NFT [tests](https://github.com/metaplex-foundation/metaplex-program-library/tree/master/bubblegum/js/tests)
|
|
|
|
## Build from Source
|
|
|
|
0. Install dependencies with `yarn`.
|
|
|
|
1. Generate the Solita SDK with `yarn solita`.
|
|
|
|
2. Then build the SDK with `yarn build`.
|
|
|
|
3. Run tests with `yarn test`. (Expect `jest` to detect an open handle that prevents it from exiting naturally) |