diff --git a/contracts/tests/gummyroll-test.ts b/contracts/tests/gummyroll-test.ts index 244b3786e18..d210589e398 100644 --- a/contracts/tests/gummyroll-test.ts +++ b/contracts/tests/gummyroll-test.ts @@ -1,24 +1,28 @@ import * as anchor from "@project-serum/anchor"; -import { BN, TransactionNamespace, InstructionNamespace, Provider, Program } from "@project-serum/anchor"; -import { Gummyroll } from "../target/types/gummyroll"; +import { BN, Provider, Program } from "@project-serum/anchor"; +import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; import { Connection, PublicKey, Keypair, SystemProgram, Transaction, - TransactionInstruction, Connection as web3Connection + Connection as web3Connection } from "@solana/web3.js"; import { assert } from "chai"; import * as crypto from 'crypto'; import { buildTree, hash, getProofOfLeaf, updateTree, Tree } from "./merkle-tree"; import { + Gummyroll, + createReplaceIx, + createAppendIx, + createTransferAuthorityIx, decodeMerkleRoll, getMerkleRollAccountSize, -} from "./merkle-roll-serde"; -import { logTx } from "./utils"; -import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; + createVerifyLeafIx, +} from '../sdk/gummyroll'; +import { execute } from "./utils"; // @ts-ignore let Gummyroll; @@ -139,42 +143,6 @@ describe("gummyroll", () => { return [merkleRollKeypair, tree] } - function createReplaceIx( - previousLeaf: Buffer, - newLeaf: Buffer, - index: number, - offChainTree: Tree, - merkleTreeKey: PublicKey, - payer: Keypair, - minimizeProofLength: boolean = false, - treeHeight: number = -1, - ): TransactionInstruction { - const proof = getProofOfLeaf(offChainTree, index, minimizeProofLength, treeHeight); - - const nodeProof = proof.map((offChainTreeNode) => { - return { - pubkey: new PublicKey(offChainTreeNode.node), - isSigner: false, - isWritable: false, - }; - }); - - return Gummyroll.instruction.replaceLeaf( - Array.from(offChainTree.root), - Array.from(previousLeaf), - Array.from(newLeaf), - index, - { - accounts: { - merkleRoll: merkleTreeKey, - authority: payer.publicKey, - }, - signers: [payer], - remainingAccounts: nodeProof, - } - ); - } - beforeEach(async () => { payer = Keypair.generate(); connection = new web3Connection( @@ -198,30 +166,58 @@ describe("gummyroll", () => { [merkleRollKeypair, offChainTree] = await createTreeOnChain(payer, 1); }); it("Append single leaf", async () => { - const newLeaf = hash( - payer.publicKey.toBuffer(), - payer.publicKey.toBuffer() + const newLeaf = crypto.randomBytes(32); + const appendIx = createAppendIx( + Gummyroll, + newLeaf, + payer, + payer, + merkleRollKeypair.publicKey ); - const appendIx = Gummyroll.instruction.append( - Array.from(newLeaf), - { - accounts: { - merkleRoll: merkleRollKeypair.publicKey, - authority: payer.publicKey, - appendAuthority: payer.publicKey, - }, - signers: [payer], - } + await execute(Gummyroll.provider, [appendIx], [payer]); + + updateTree(offChainTree, newLeaf, 1); + + const merkleRollAccount = await Gummyroll.provider.connection.getAccountInfo( + merkleRollKeypair.publicKey ); + const merkleRoll = decodeMerkleRoll(merkleRollAccount.data); + const onChainRoot = + merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); - const tx = new Transaction().add(appendIx); - const txid = await Gummyroll.provider.send(tx, [payer], { - commitment: "confirmed", - }); - await logTx(Gummyroll.provider, txid, false); + assert( + Buffer.from(onChainRoot).equals(offChainTree.root), + "Updated on chain root matches root of updated off chain tree" + ); + }); + it("Verify proof works for that leaf", async () => { + const previousLeaf = offChainTree.leaves[0].node; + const newLeaf = crypto.randomBytes(32); + const index = 0; + const proof = getProofOfLeaf(offChainTree, index).map((treeNode) => { return treeNode.node }); - updateTree(offChainTree, newLeaf, 1); + const verifyLeafIx = createVerifyLeafIx( + Gummyroll, + merkleRollKeypair.publicKey, + offChainTree.root, + previousLeaf, + index, + proof + ); + const replaceLeafIx = createReplaceIx( + Gummyroll, + payer, + merkleRollKeypair.publicKey, + offChainTree.root, + previousLeaf, + newLeaf, + index, + proof + ); + await execute(Gummyroll.provider, [verifyLeafIx, replaceLeafIx], [payer]); + + updateTree(offChainTree, newLeaf, index); const merkleRollAccount = await Gummyroll.provider.connection.getAccountInfo( merkleRollKeypair.publicKey @@ -235,19 +231,76 @@ describe("gummyroll", () => { "Updated on chain root matches root of updated off chain tree" ); }); + it("Verify leaf fails when proof fails", async () => { + const previousLeaf = offChainTree.leaves[0].node; + const newLeaf = crypto.randomBytes(32); + const index = 0; + // Proof has random bytes: definitely wrong + const proof = getProofOfLeaf(offChainTree, index).map( + (treeNode) => { return crypto.randomBytes(32) } + ); + + // Verify proof is invalid + const verifyLeafIx = createVerifyLeafIx( + Gummyroll, + merkleRollKeypair.publicKey, + offChainTree.root, + previousLeaf, + index, + proof + ); + try { + await execute(Gummyroll.provider, [verifyLeafIx], [payer]); + assert(false, "Proof should have failed to verify"); + } catch { + } + + // Replace instruction with same proof fails + const replaceLeafIx = createReplaceIx( + Gummyroll, + payer, + merkleRollKeypair.publicKey, + offChainTree.root, + previousLeaf, + newLeaf, + index, + proof + ); + try { + await execute(Gummyroll.provider, [replaceLeafIx], [payer]); + assert(false, "Replace should have failed to verify"); + } catch { + } + const merkleRollAccount = await Gummyroll.provider.connection.getAccountInfo( + merkleRollKeypair.publicKey + ); + const merkleRoll = decodeMerkleRoll(merkleRollAccount.data); + const onChainRoot = + merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); + + assert( + Buffer.from(onChainRoot).equals(offChainTree.root), + "Updated on chain root matches root of updated off chain tree" + ); + }); it("Replace that leaf", async () => { const previousLeaf = offChainTree.leaves[0].node; const newLeaf = crypto.randomBytes(32); const index = 0; - const replaceLeafIx = createReplaceIx(previousLeaf, newLeaf, index, offChainTree, merkleRollKeypair.publicKey, payer); + const replaceLeafIx = createReplaceIx( + Gummyroll, + payer, + merkleRollKeypair.publicKey, + offChainTree.root, + previousLeaf, + newLeaf, + index, + getProofOfLeaf(offChainTree, index, false, -1).map((treeNode) => { return treeNode.node }) + ); assert(replaceLeafIx.keys.length == (2 + MAX_DEPTH), `Failed to create proof for ${MAX_DEPTH}`); - const tx = new Transaction().add(replaceLeafIx); - const txid = await Gummyroll.provider.send(tx, [payer], { - commitment: "confirmed", - }); - await logTx(Gummyroll.provider, txid, false); + await execute(Gummyroll.provider, [replaceLeafIx], [payer]); updateTree(offChainTree, newLeaf, index); @@ -259,7 +312,6 @@ describe("gummyroll", () => { merkleRoll.roll.changeLogs[merkleRoll.roll.activeIndex].root.toBuffer(); assert( - Buffer.from(onChainRoot).equals(offChainTree.root), "Updated on chain root matches root of updated off chain tree" ); @@ -270,21 +322,18 @@ describe("gummyroll", () => { const newLeaf = crypto.randomBytes(32); const index = 0; - const replaceLeafIx = createReplaceIx(previousLeaf, + const replaceLeafIx = createReplaceIx( + Gummyroll, + payer, + merkleRollKeypair.publicKey, + offChainTree.root, + previousLeaf, newLeaf, index, - offChainTree, - merkleRollKeypair.publicKey, - payer, - true, - 1 + getProofOfLeaf(offChainTree, index, true, 1).map((treeNode) => { return treeNode.node }) ); assert(replaceLeafIx.keys.length == (2 + 1), "Failed to minimize proof to expected size of 1"); - const tx = new Transaction().add(replaceLeafIx); - const txid = await Gummyroll.provider.send(tx, [payer], { - commitment: "confirmed", - }); - await logTx(Gummyroll.provider, txid, false); + await execute(Gummyroll.provider, [replaceLeafIx], [payer]); updateTree(offChainTree, newLeaf, index); @@ -302,6 +351,172 @@ describe("gummyroll", () => { }); }); + describe("Examples tranferring appendAuthority", () => { + const authority = Keypair.generate(); + const randomSigner = Keypair.generate(); + describe("Examples transferring appendAuthority", () => { + it("... initializing tree ...", async () => { + await (connection as Connection).requestAirdrop(authority.publicKey, 1e10); + [merkleRollKeypair, offChainTree] = await createTreeOnChain(authority, 1); + }) + it("Attempting to append without appendAuthority fails", async () => { + // Random leaf + const newLeaf = crypto.randomBytes(32); + const appendIx = createAppendIx(Gummyroll, newLeaf, authority, randomSigner, merkleRollKeypair.publicKey); + + try { + await execute(Gummyroll.provider, [appendIx], [payer, randomSigner]); + assert(false, "Transaction should have failed, since `randomSigner` is not append authority") + } catch { + } + }); + it("But authority can transfer appendAuthority", async () => { + const transferAppendAuthorityIx = createTransferAuthorityIx( + Gummyroll, + authority, + merkleRollKeypair.publicKey, + null, + randomSigner.publicKey, + ); + await execute(Gummyroll.provider, [transferAppendAuthorityIx], [authority]); + + const merkleRoll = decodeMerkleRoll( + ( + await Gummyroll.provider.connection.getAccountInfo( + merkleRollKeypair.publicKey + ) + ).data + ); + const merkleRollInfo = merkleRoll.header; + + assert( + merkleRollInfo.authority.equals(authority.publicKey), + `Upon transfering appendAuthority, authority should be ${authority.publicKey.toString()}, but was instead updated to ${merkleRollInfo.authority.toString()}` + ); + assert( + merkleRollInfo.appendAuthority.equals(randomSigner.publicKey), + `Upon transferring appendAuthority, appendAuthority should be ${randomSigner.publicKey.toString()} but is ${merkleRollInfo.appendAuthority.toString()}` + ); + }); + it("So the new appendAuthority can append", async () => { + const newLeaf = crypto.randomBytes(32); + const appendIx = createAppendIx(Gummyroll, newLeaf, authority, randomSigner, merkleRollKeypair.publicKey); + await execute(Gummyroll.provider, [appendIx], [authority, randomSigner]); + + const merkleRoll = decodeMerkleRoll( + ( + await Gummyroll.provider.connection.getAccountInfo( + merkleRollKeypair.publicKey + ) + ).data + ); + assert( + merkleRoll.roll.rightMostPath.index === 2, + `Expected merkle roll to now have 2 leaves after append, but only has ${merkleRoll.roll.rightMostPath.index}` + ); + + updateTree(offChainTree, newLeaf, 1); + + }); + it("but not replace", async () => { + const newLeaf = crypto.randomBytes(32); + const replaceIx = createReplaceIx( + Gummyroll, + randomSigner, + merkleRollKeypair.publicKey, + offChainTree.root, + offChainTree.leaves[1].node, + newLeaf, + 1, + getProofOfLeaf(offChainTree, 1).map((treeNode) => { return treeNode.node }) + ); + try { + await execute(Gummyroll.provider, [replaceIx], [randomSigner]) + assert(false, "Transaction should have failed since the append authority cannot act as the authority for replaces") + } catch { + } + }); + }); + describe("Examples transferring authority", () => { + it("... initializing tree ...", async () => { + await (connection as Connection).requestAirdrop(authority.publicKey, 1e10); + [merkleRollKeypair, offChainTree] = await createTreeOnChain(authority, 1); + }) + it("Attempting to append without appendAuthority fails", async () => { + await (connection as Connection).requestAirdrop(randomSigner.publicKey, 1e10); + + const newLeaf = crypto.randomBytes(32); + const replaceIndex = 0; + const proof = getProofOfLeaf(offChainTree, replaceIndex); + const replaceIx = createReplaceIx( + Gummyroll, + randomSigner, + merkleRollKeypair.publicKey, + offChainTree.root, + offChainTree.leaves[replaceIndex].node, + newLeaf, + replaceIndex, + proof.map((treeNode) => { return treeNode.node }) + ); + + try { + await execute(Gummyroll.provider, [replaceIx], [randomSigner]) + assert(false, "Transaction should have failed since incorrect authority cannot execute replaces") + } catch { + } + }); + it("Can transfer authority", async () => { + const transferAppendAuthorityIx = createTransferAuthorityIx( + Gummyroll, + authority, + merkleRollKeypair.publicKey, + randomSigner.publicKey, + null, + ); + await execute(Gummyroll.provider, [transferAppendAuthorityIx], [authority]); + + const merkleRoll = decodeMerkleRoll( + ( + await Gummyroll.provider.connection.getAccountInfo( + merkleRollKeypair.publicKey + ) + ).data + ); + const merkleRollInfo = merkleRoll.header; + + assert( + merkleRollInfo.authority.equals(randomSigner.publicKey), + `Upon transfering appendAuthority, authority should be ${randomSigner.publicKey.toString()}, but was instead updated to ${merkleRollInfo.authority.toString()}` + ); + assert( + merkleRollInfo.appendAuthority.equals(authority.publicKey), + `Upon transferring appendAuthority, appendAuthority should be ${authority.publicKey.toString()} but is ${merkleRollInfo.appendAuthority.toString()}` + ); + }); + it("Attempting to replace with new authority now works", async () => { + const newLeaf = crypto.randomBytes(32); + const replaceIndex = 0; + const proof = getProofOfLeaf(offChainTree, replaceIndex); + const replaceIx = createReplaceIx( + Gummyroll, + randomSigner, + merkleRollKeypair.publicKey, + offChainTree.root, + offChainTree.leaves[replaceIndex].node, + newLeaf, + replaceIndex, + proof.map((treeNode) => { return treeNode.node }) + ); + + try { + await execute(Gummyroll.provider, [replaceIx], [randomSigner]) + assert(false, "Transaction should have failed since incorrect authority cannot execute replaces") + } catch { + } + }); + }); + }); + describe(`Having created a tree with ${MAX_SIZE} leaves`, () => { beforeEach(async () => { [merkleRollKeypair, offChainTree] = await createTreeOnChain(payer, MAX_SIZE); @@ -322,27 +537,15 @@ describe("gummyroll", () => { ); leavesToUpdate.push(newLeaf); const proof = getProofOfLeaf(offChainTree, index); - - const nodeProof = proof.map((offChainTreeNode) => { - return { - pubkey: new PublicKey(offChainTreeNode.node), - isSigner: false, - isWritable: false, - }; - }); - const replaceIx = Gummyroll.instruction.replaceLeaf( - Array.from(offChainTree.root), - Array.from(offChainTree.leaves[i].node), - Array.from(newLeaf), + const replaceIx = createReplaceIx( + Gummyroll, + payer, + merkleRollKeypair.publicKey, + offChainTree.root, + offChainTree.leaves[i].node, + newLeaf, index, - { - accounts: { - merkleRoll: merkleRollKeypair.publicKey, - authority: payer.publicKey, - }, - signers: [payer], - remainingAccounts: nodeProof, - } + proof.map((treeNode) => { return treeNode.node }) ); ixArray.push(replaceIx); }; @@ -387,21 +590,9 @@ describe("gummyroll", () => { [merkleRollKeypair, offChainTree] = await createTreeOnChain(payer, 0, DEPTH, 2 ** DEPTH); for (let i = 0; i < 2 ** DEPTH; i++) { - const appendIx = Gummyroll.instruction.append( - Array.from(Buffer.alloc(32, i + 1)), - { - accounts: { - merkleRoll: merkleRollKeypair.publicKey, - authority: payer.publicKey, - appendAuthority: payer.publicKey, - }, - signers: [payer], - } - ); - const tx = new Transaction().add(appendIx); - await Gummyroll.provider.send(tx, [payer], { - commitment: "confirmed", - }); + const newLeaf = Array.from(Buffer.alloc(32, i + 1)); + const appendIx = createAppendIx(Gummyroll, newLeaf, payer, payer, merkleRollKeypair.publicKey) + await execute(Gummyroll.provider, [appendIx], [payer]); } // Compare on-chain & off-chain roots @@ -428,28 +619,23 @@ describe("gummyroll", () => { const maliciousLeafHash1 = crypto.randomBytes(32); const nodeProof = []; for (let i = 0; i < DEPTH; i++) { - nodeProof.push({ pubkey: new PublicKey(Buffer.alloc(32)), isSigner: false, isWritable: false }); + nodeProof.push(Buffer.alloc(32)); } - const replaceIx = Gummyroll.instruction.replaceLeaf( - // Root - make this nonsense so it won't match what's in CL, and force proof autocompletion + // Root - make this nonsense so it won't match what's in CL, and force proof autocompletion + const replaceIx = createReplaceIx( + Gummyroll, + payer, + merkleRollKeypair.publicKey, Buffer.alloc(32), maliciousLeafHash, maliciousLeafHash1, 0, - { - accounts: { - merkleRoll: merkleRollKeypair.publicKey, - authority: payer.publicKey, - }, - signers: [payer], - remainingAccounts: nodeProof, - } + nodeProof, ); - const tx = new Transaction().add(replaceIx); try { - await Gummyroll.provider.send(tx, [payer], { commitment: "confirmed" }); + await execute(Gummyroll.provider, [replaceIx], [payer]) assert(false, "Attacker was able to succesfully write fake existence of a leaf"); } catch (e) {