Skip to content

Commit

Permalink
Merge pull request solana-labs#81 from jarry-xiao/gummyroll
Browse files Browse the repository at this point in the history
Add Gummyroll instructions
  • Loading branch information
ngundotra authored Jun 2, 2022
2 parents 4c3e115 + 3091c46 commit 81cecaf
Show file tree
Hide file tree
Showing 12 changed files with 560 additions and 152 deletions.
2 changes: 1 addition & 1 deletion contracts/programs/gummyroll/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub enum GummyrollError {
}

impl From<&CMTError> for GummyrollError {
fn from(error: &CMTError) -> Self {
fn from(_error: &CMTError) -> Self {
GummyrollError::ConcurrentMerkleTreeError
}
}
111 changes: 108 additions & 3 deletions contracts/programs/gummyroll/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ pub struct Append<'info> {
pub append_authority: Signer<'info>,
}

#[derive(Accounts)]
pub struct VerifyLeaf<'info> {
/// CHECK: This account is validated in the instruction
pub merkle_roll: UncheckedAccount<'info>,
}

#[derive(Accounts)]
pub struct TransferAuthority<'info> {
#[account(mut)]
/// CHECK: This account is validated in the instruction
pub merkle_roll: UncheckedAccount<'info>,
pub authority: Signer<'info>,
}

/// This macro applies functions on a merkle roll and emits leaf information
/// needed to sync the merkle tree state with off-chain indexers.
macro_rules! merkle_roll_depth_size_apply_fn {
($max_depth:literal, $max_size:literal, $emit_msg:ident, $id:ident, $bytes:ident, $func:ident, $($arg:tt)*) => {
if size_of::<MerkleRoll::<$max_depth, $max_size>>() != $bytes.len() {
Expand Down Expand Up @@ -74,6 +90,9 @@ macro_rules! merkle_roll_depth_size_apply_fn {
}
}

/// This applies a given function on a merkle roll by
/// allowing the compiler to infer the size of the tree based
/// upon the header information stored on-chain
macro_rules! merkle_roll_apply_fn {
($header:ident, $emit_msg:ident, $id:ident, $bytes:ident, $func:ident, $($arg:tt)*) => {
// Note: max_buffer_size MUST be a power of 2
Expand Down Expand Up @@ -111,6 +130,16 @@ macro_rules! merkle_roll_apply_fn {
pub mod gummyroll {
use super::*;

/// Creates a new merkle tree with maximum leaf capacity of power(2, max_depth)
/// and a minimum concurrency limit of max_buffer_size.
///
/// Concurrency limit represents the # of replace instructions that can be successfully
/// executed with proofs dated for the same root. For example, a maximum buffer size of 1024
/// means that a minimum of 1024 replaces can be executed before a new proof must be
/// generated for the next replace instruction.
///
/// Concurrency limit should be determined by empirically testing the demand for
/// state built on top of gummyroll.
pub fn init_empty_gummyroll(
ctx: Context<Initialize>,
max_depth: u32,
Expand All @@ -133,6 +162,12 @@ pub mod gummyroll {
merkle_roll_apply_fn!(header, true, id, roll_bytes, initialize,)
}

/// Note:
/// Supporting this instruction open a security vulnerability for indexers.
/// This instruction has been deemed unusable for publicly indexed compressed NFTs.
/// Indexing batched data in this way requires indexers to read in the `uri`s onto physical storage
/// and then into their database. This opens up a DOS attack vector, whereby this instruction is
/// repeatedly invoked, causing indexers to fail.
pub fn init_gummyroll_with_root(
ctx: Context<Initialize>,
max_depth: u32,
Expand Down Expand Up @@ -174,11 +209,14 @@ pub mod gummyroll {
initialize_with_root,
root,
leaf,
proof,
&proof,
index
)
}

/// Executes an instruction that overwrites a leaf node.
/// Composing programs should check that the data hashed into previous_leaf
/// matches the authority information necessary to execute this instruction.
pub fn replace_leaf(
ctx: Context<Modify>,
root: [u8; 32],
Expand Down Expand Up @@ -209,11 +247,74 @@ pub mod gummyroll {
root,
previous_leaf,
new_leaf,
proof,
&proof,
index
)
}

/// Transfers authority or append authority
/// requires `authority` to sign
pub fn transfer_authority(
ctx: Context<TransferAuthority>,
new_authority: Option<Pubkey>,
new_append_authority: Option<Pubkey>,
) -> Result<()> {
let mut merkle_roll_bytes = ctx.accounts.merkle_roll.try_borrow_mut_data()?;
let (mut header_bytes, _) = merkle_roll_bytes.split_at_mut(size_of::<MerkleRollHeader>());

let mut header = Box::new(MerkleRollHeader::try_from_slice(header_bytes)?);
assert_eq!(header.authority, ctx.accounts.authority.key());

match new_authority {
Some(new_auth) => {
header.authority = new_auth;
msg!("Authority transferred to: {:?}", header.authority);
}
_ => {}
}
match new_append_authority {
Some(new_append_auth) => {
header.append_authority = new_append_auth;
msg!(
"Append authority transferred to: {:?}",
header.append_authority
);
}
_ => {}
}
header.serialize(&mut header_bytes)?;

Ok(())
}

/// If proof is invalid, error is thrown
pub fn verify_leaf(
ctx: Context<VerifyLeaf>,
root: [u8; 32],
leaf: [u8; 32],
index: u32,
) -> Result<()> {
let mut merkle_roll_bytes = ctx.accounts.merkle_roll.try_borrow_mut_data()?;
let (header_bytes, roll_bytes) =
merkle_roll_bytes.split_at_mut(size_of::<MerkleRollHeader>());

let header = Box::new(MerkleRollHeader::try_from_slice(header_bytes)?);

let mut proof = vec![];
for node in ctx.remaining_accounts.iter() {
proof.push(node.key().to_bytes());
}

let id = ctx.accounts.merkle_roll.key();

merkle_roll_apply_fn!(header, false, id, roll_bytes, prove_leaf, root, leaf, &proof, index)
}

/// This instruction allows the tree's mint_authority to append a new leaf to the tree
/// without having to supply a valid proof.
///
/// This is accomplished by using the rightmost_proof of the merkle roll to construct a
/// valid proof, and then updating the rightmost_proof for the next leaf if possible.
pub fn append(ctx: Context<Append>, leaf: [u8; 32]) -> Result<()> {
let mut merkle_roll_bytes = ctx.accounts.merkle_roll.try_borrow_mut_data()?;
let (header_bytes, roll_bytes) =
Expand All @@ -227,6 +328,10 @@ pub mod gummyroll {
merkle_roll_apply_fn!(header, true, id, roll_bytes, append, leaf)
}

/// This instruction takes a proof, and will attempt to write the given leaf
/// to the specified index in the tree. If the insert operation fails, the leaf will be `append`-ed
/// to the tree.
/// It is up to the indexer to parse the final location of the leaf from the emitted changelog.
pub fn insert_or_append(
ctx: Context<Modify>,
root: [u8; 32],
Expand Down Expand Up @@ -255,7 +360,7 @@ pub mod gummyroll {
fill_empty_or_append,
root,
leaf,
proof,
&proof,
index
)
}
Expand Down
4 changes: 1 addition & 3 deletions contracts/programs/gummyroll/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use crate::error::GummyrollError;
use anchor_lang::{
prelude::*,
solana_program::{msg, program_error::ProgramError},
};
use bytemuck::{Pod, PodCastError};
use concurrent_merkle_tree::{merkle_roll::MerkleRoll, state::Node};
use concurrent_merkle_tree::merkle_roll::MerkleRoll;
use std::any::type_name;
use std::convert::TryInto;
use std::mem::size_of;

pub trait ZeroCopy: Pod {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { assertConfirmedTransaction } from '@metaplex-foundation/amman';
import {
PublicKey
} from '@solana/web3.js';
import * as borsh from 'borsh';
import { BN } from '@project-serum/anchor';


// maxDepth: number, // u32
// maxBufferSize: number, // u32
// authority: PublicKey,

/**
* Manually create a model for MerkleRoll in order to deserialize correctly
*/
Expand All @@ -35,7 +29,7 @@ type MerkleRoll = {

type ChangeLog = {
root: PublicKey,
pathNodes: PublicKey[]
pathNodes: PublicKey[]
index: number, // u32
_padding: number, // u32
}
Expand Down Expand Up @@ -114,9 +108,9 @@ export function decodeMerkleRoll(buffer: Buffer): OnChainMerkleRoll {
}

export function getMerkleRollAccountSize(maxDepth: number, maxBufferSize: number): number {
let headerSize = 8 + 32 + 32;
let changeLogSize = (maxDepth * 32 + 32 + 4 + 4) * maxBufferSize;
let rightMostPathSize = maxDepth * 32 + 32 + 4 + 4;
let merkleRollSize = 8 + 8 + 16 + changeLogSize + rightMostPathSize;
return merkleRollSize + headerSize;
}
let headerSize = 8 + 32 + 32;
let changeLogSize = (maxDepth * 32 + 32 + 4 + 4) * maxBufferSize;
let rightMostPathSize = maxDepth * 32 + 32 + 4 + 4;
let merkleRollSize = 8 + 8 + 16 + changeLogSize + rightMostPathSize;
return merkleRollSize + headerSize;
}
3 changes: 3 additions & 0 deletions contracts/sdk/gummyroll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './instructions';
export * from './accounts';
export * from './types';
105 changes: 105 additions & 0 deletions contracts/sdk/gummyroll/instructions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Program } from '@project-serum/anchor';
import { Gummyroll } from "../types";
import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js';

export function createReplaceIx(
gummyroll: Program<Gummyroll>,
authority: Keypair,
merkleRoll: PublicKey,
treeRoot: Buffer,
previousLeaf: Buffer,
newLeaf: Buffer,
index: number,
proof: Buffer[]
): TransactionInstruction {
const nodeProof = proof.map((node) => {
return {
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
};
});
return gummyroll.instruction.replaceLeaf(
Array.from(treeRoot),
Array.from(previousLeaf),
Array.from(newLeaf),
index,
{
accounts: {
merkleRoll,
authority: authority.publicKey,
},
signers: [authority],
remainingAccounts: nodeProof,
}
);
}

export function createAppendIx(
gummyroll: Program<Gummyroll>,
newLeaf: Buffer | ArrayLike<number>,
authority: Keypair,
appendAuthority: Keypair,
merkleRoll: PublicKey,
): TransactionInstruction {
return gummyroll.instruction.append(
Array.from(newLeaf),
{
accounts: {
merkleRoll,
authority: authority.publicKey,
appendAuthority: appendAuthority.publicKey,
},
signers: [authority, appendAuthority],
}
);
}

export function createTransferAuthorityIx(
gummyroll: Program<Gummyroll>,
authority: Keypair,
merkleRoll: PublicKey,
newAuthority: PublicKey | null,
newAppendAuthority: PublicKey | null,
): TransactionInstruction {
return gummyroll.instruction.transferAuthority(
newAuthority,
newAppendAuthority,
{
accounts: {
merkleRoll,
authority: authority.publicKey,
},
signers: [authority],
}
);
}

export function createVerifyLeafIx(
gummyroll: Program<Gummyroll>,
merkleRoll: PublicKey,
root: Buffer,
leaf: Buffer,
index: number,
proof: Buffer[],
): TransactionInstruction {
const nodeProof = proof.map((node) => {
return {
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
};
});
return gummyroll.instruction.verifyLeaf(
Array.from(root),
Array.from(leaf),
index,
{
accounts: {
merkleRoll
},
signers: [],
remainingAccounts: nodeProof,
}
);
}
1 change: 1 addition & 0 deletions contracts/sdk/gummyroll/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Gummyroll } from "../../../target/types/gummyroll";
2 changes: 1 addition & 1 deletion contracts/tests/bubblegum-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { buildTree, Tree } from "./merkle-tree";
import {
decodeMerkleRoll,
getMerkleRollAccountSize,
} from "./merkle-roll-serde";
} from "../sdk/gummyroll";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
Expand Down
3 changes: 1 addition & 2 deletions contracts/tests/continuous_gummyroll-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as anchor from "@project-serum/anchor";
import { Gummyroll } from "../target/types/gummyroll";
import { Program, Provider, } from "@project-serum/anchor";
import {
Connection as web3Connection,
Expand All @@ -11,7 +10,7 @@ import {
import { assert } from "chai";

import { buildTree, getProofOfLeaf, updateTree, Tree, getProofOfAssetFromServer, checkProof } from "./merkle-tree";
import { decodeMerkleRoll, getMerkleRollAccountSize } from "./merkle-roll-serde";
import { Gummyroll, decodeMerkleRoll, getMerkleRollAccountSize } from "../sdk/gummyroll";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";

const HOST = "127.0.0.1";
Expand Down
Loading

0 comments on commit 81cecaf

Please sign in to comment.