Преглед изворни кода

Create PDA hash table example (#1438)

Signed-off-by: Lucas Steuernagel <lucas.tnagel@gmail.com>
Lucas Steuernagel пре 2 година
родитељ
комит
cefc34568d

+ 11 - 0
docs/examples.rst

@@ -36,3 +36,14 @@ to be the NFT itself. It can mint itself and transfer ownership. It also stores
 Please, check `simple_collectible.sol <https://github.com/hyperledger/solang/blob/main/integration/solana/simple_collectible.sol>`_
 for the Solidity contract and `simple_collectible.spec.ts <https://github.com/hyperledger/solang/blob/main/integration/solana/simple_collectible.spec.ts>`_
 for the Typescript code that interacts with Solidity.
+
+
+PDA Hash Table
+______________
+
+On Solana, it is possible to create a hash table on chain with program derived addresses (PDA). This is done by
+using the intended key as the seed for finding the PDA. There is an example of how one can achieve so in our integration
+tests. Please, check `UserStats.sol <https://github.com/hyperledger/solang/blob/main/integration/solana/UserStats.sol>`_
+for the Solidity contract and `user_stats.spec.ts <https://github.com/hyperledger/solang/blob/main/integration/solana/user_stats.spec.ts>`_
+for the client code, which contains most of the explanations about how the table works. This example was inspired by
+`Anchor's PDA hash table <https://www.anchor-lang.com/docs/pdas#hashmap-like-structures-using-pd-as>`_.

+ 1 - 0
integration/solana/UserStats.key

@@ -0,0 +1 @@
+[71,111,159,74,62,29,145,33,39,96,158,63,115,65,118,23,2,210,104,73,172,92,61,142,21,132,76,189,217,64,174,182,227,224,93,34,40,170,119,26,112,243,87,225,85,23,105,182,7,158,10,174,153,76,114,42,141,51,72,20,71,17,191,223]

+ 33 - 0
integration/solana/UserStats.sol

@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+
+@program_id("GLXybr8w3egyd8HpnHJEy6vQUoXyD3uGzjoUcAnmjQwx")
+contract UserStats {
+    string private name;
+    uint16 private level;
+    uint8 private bump;
+
+    // The constructor initializes the PDA hash table for a user.
+    @payer(wallet)
+    @seed("user-stats")
+    @space(250)
+    constructor(@seed bytes user_key, @bump uint8 _bump, string _name, uint16 _level) {
+        name = _name;
+        level = _level;
+        bump = _bump;
+    }
+
+    // Change the name saved in the data account
+    function change_user_name(string new_name) external {
+        name = new_name;
+    }
+
+    // Change the level saved in the data account
+    function change_level(uint16 new_level) external {
+        level = new_level;
+    }
+
+    // Read the information from the data account
+    function return_stats() external view returns (string memory, uint16, uint8) {
+        return (name, level, bump);
+    }
+}

+ 2 - 2
integration/solana/balances.spec.ts

@@ -2,14 +2,14 @@
 
 import expect from 'expect';
 import { Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import { BN } from '@coral-xyz/anchor';
 
 describe('Deploy solang contract and test', function () {
     this.timeout(500000);
 
     it('balances', async function () {
-        let { program, storage, payer, provider } = await loadContract('balances', []);
+        let { program, storage, payer, provider } = await loadContractAndCallConstructor('balances', []);
 
         let res = await program.methods.getBalance(payer.publicKey)
             .accounts({ dataAccount: storage.publicKey })

+ 2 - 2
integration/solana/builtins.spec.ts

@@ -1,14 +1,14 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import { AccountMeta, SYSVAR_CLOCK_PUBKEY, PublicKey } from '@solana/web3.js';
 
 describe('Testing builtins', function () {
     this.timeout(500000);
 
     it('builtins', async function () {
-        let { program, provider, storage } = await loadContract('builtins');
+        let { program, provider, storage } = await loadContractAndCallConstructor('builtins');
 
         let res = await program.methods.hashRipemd160(Buffer.from('Call me Ishmael.', 'utf8')).view();
         expect(Buffer.from(res).toString("hex")).toBe("0c8b641c461e3c7abbdabd7f12a8905ee480dadf");

+ 2 - 2
integration/solana/calls.spec.ts

@@ -1,14 +1,14 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import expect from 'expect';
-import { loadContract, loadContractWithProvider } from './setup';
+import { loadContractAndCallConstructor, loadContractWithProvider } from './setup';
 import { BN } from '@coral-xyz/anchor';
 
 describe('Testing calls', function () {
     this.timeout(100000);
 
     it('external_call', async function () {
-        let caller = await loadContract('caller');
+        let caller = await loadContractAndCallConstructor('caller');
 
         const provider = caller.provider;
 

+ 2 - 2
integration/solana/create_contract.spec.ts

@@ -3,7 +3,7 @@
 import { Connection, Keypair, PublicKey, sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js';
 import expect from 'expect';
 import { Program, Provider, BN } from '@coral-xyz/anchor';
-import { create_account, loadContract } from './setup';
+import { create_account, loadContractAndCallConstructor } from './setup';
 import fs from 'fs';
 
 describe('ChildContract', function () {
@@ -15,7 +15,7 @@ describe('ChildContract', function () {
     let provider: Provider;
 
     before(async function () {
-        ({ program, storage, payer, provider } = await loadContract('creator'));
+        ({ program, storage, payer, provider } = await loadContractAndCallConstructor('creator'));
     });
 
     it('Create Contract', async function () {

+ 2 - 2
integration/solana/errors.spec.ts

@@ -2,13 +2,13 @@
 
 import { BN } from '@coral-xyz/anchor';
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 
 describe('Testing errors', function () {
     this.timeout(500000);
 
     it('errors', async function () {
-        const { program, storage } = await loadContract('errors');
+        const { program, storage } = await loadContractAndCallConstructor('errors');
 
         let res = await program.methods.doRevert(false).view();
 

+ 2 - 2
integration/solana/events.spec.ts

@@ -1,13 +1,13 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 
 describe('Test events', function () {
     this.timeout(500000);
 
     it('events', async function () {
-        const { program, storage } = await loadContract('MyContractEvents');
+        const { program, storage } = await loadContractAndCallConstructor('MyContractEvents');
 
         const res = await program.methods.test()
             .accounts({ dataAccount: storage.publicKey })

+ 2 - 2
integration/solana/overflow.spec.ts

@@ -1,14 +1,14 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import { BN } from '@coral-xyz/anchor';
 
 describe('Testing math overflow', function () {
     this.timeout(500000);
 
     it('overflow', async function () {
-        let { program } = await loadContract('overflow');
+        let { program } = await loadContractAndCallConstructor('overflow');
 
         let res = await program.methods.addu32(new BN(1), new BN(3)).view();
         expect(res).toEqual(4);

+ 2 - 2
integration/solana/oznfc.spec.ts

@@ -1,13 +1,13 @@
 // SPDX-License-Identifier: Apache-2.0
 
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 
 describe('Deploy solang contract and test', function () {
     this.timeout(500000);
 
     it('Events', async function () {
-        const { program, storage } = await loadContract('Events');
+        const { program, storage } = await loadContractAndCallConstructor('Events');
 
         let res = await program.methods.getName()
             .accounts({ dataAccount: storage.publicKey })

+ 2 - 2
integration/solana/runtime_errors.spec.ts

@@ -2,7 +2,7 @@
 
 import { Keypair, PublicKey, sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js';
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import { Program, Provider, BN, AnchorProvider } from '@coral-xyz/anchor';
 import { createAccount } from "@solana/spl-token";
 
@@ -15,7 +15,7 @@ describe('Runtime Errors', function () {
     let provider: Provider;
 
     before(async function () {
-        ({ program, storage, payer, provider } = await loadContract('RuntimeErrors'));
+        ({ program, storage, payer, provider } = await loadContractAndCallConstructor('RuntimeErrors'));
     });
 
     it('Prints runtime errors', async function () {

+ 17 - 14
integration/solana/setup.ts

@@ -6,9 +6,23 @@ import fs from 'fs';
 
 const endpoint: string = process.env.RPC_URL || "http://127.0.0.1:8899";
 
-export async function loadContract(name: string, args: any[] = [], space: number = 8192):
+export async function loadContractAndCallConstructor(name: string, args: any[] = [], space: number = 8192):
     Promise<{ program: Program, payer: Keypair, provider: AnchorProvider, storage: Keypair, program_key: PublicKey }> {
 
+    const {program, payer, provider, program_key} = await loadContract(name);
+
+    const storage = Keypair.generate();
+    await create_account(storage, program_key, space);
+
+    await program.methods.new(...args)
+        .accounts({ dataAccount: storage.publicKey })
+        .rpc();
+
+    return { provider, program, payer, storage, program_key: program_key };
+}
+
+export async function loadContract(name: string):
+    Promise<{program: Program, payer: Keypair, provider: AnchorProvider, program_key: PublicKey}> {
     const idl = JSON.parse(fs.readFileSync(`${name}.json`, 'utf8'));
 
     const payer = loadKey('payer.key');
@@ -16,20 +30,9 @@ export async function loadContract(name: string, args: any[] = [], space: number
     process.env['ANCHOR_WALLET'] = 'payer.key';
 
     const provider = AnchorProvider.local(endpoint);
-
-    const storage = Keypair.generate();
-
     const program_key = loadKey(`${name}.key`);
-
-    await create_account(storage, program_key.publicKey, space);
-
-    const program = new Program(idl, program_key.publicKey, provider);
-
-    await program.methods.new(...args)
-        .accounts({ dataAccount: storage.publicKey })
-        .rpc();
-
-    return { provider, program, payer, storage, program_key: program_key.publicKey };
+    const program = new Program(idl, program_key.publicKey, provider)
+    return {program, payer, provider, program_key: program_key.publicKey};
 }
 
 export async function create_account(account: Keypair, programId: PublicKey, space: number) {

+ 9 - 9
integration/solana/simple.spec.ts

@@ -1,5 +1,5 @@
 import expect from 'expect';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import crypto from 'crypto';
 import { BN } from '@coral-xyz/anchor';
 
@@ -7,7 +7,7 @@ describe('Simple solang tests', function () {
     this.timeout(500000);
 
     it('flipper', async function () {
-        let { program, storage } = await loadContract('flipper', [true]);
+        let { program, storage } = await loadContractAndCallConstructor('flipper', [true]);
 
         // make sure we can't run the constructor twice
         await expect(program.methods.new(false)
@@ -24,7 +24,7 @@ describe('Simple solang tests', function () {
     });
 
     it('primitives', async function () {
-        let { program, payer, storage } = await loadContract('primitives', []);
+        let { program, payer, storage } = await loadContractAndCallConstructor('primitives', []);
 
         // TEST Basic enums
         // in ethereum, an enum is described as an uint8 so can't use the enum
@@ -152,7 +152,7 @@ describe('Simple solang tests', function () {
     });
 
     it('store', async function () {
-        const { storage, program } = await loadContract('store', []);
+        const { storage, program } = await loadContractAndCallConstructor('store', []);
 
         let res = await program.methods.getValues1().accounts({ dataAccount: storage.publicKey }).view();
 
@@ -237,7 +237,7 @@ describe('Simple solang tests', function () {
     });
 
     it('structs', async function () {
-        const { program, storage } = await loadContract('store', []);
+        const { program, storage } = await loadContractAndCallConstructor('store', []);
 
         await program.methods.setFoo1().accounts({ dataAccount: storage.publicKey }).rpc();
 
@@ -347,13 +347,13 @@ describe('Simple solang tests', function () {
 
 
     it('account storage too small constructor', async function () {
-        await expect(loadContract('store', [], 100))
+        await expect(loadContractAndCallConstructor('store', [], 100))
             .rejects
             .toThrowError(new Error('failed to send transaction: Transaction simulation failed: Error processing Instruction 0: account data too small for instruction'));
     });
 
     it('account storage too small dynamic alloc', async function () {
-        const { program, storage } = await loadContract('store', [], 233);
+        const { program, storage } = await loadContractAndCallConstructor('store', [], 233);
 
         // storage.sol needs 168 bytes on constructor, more for string data
 
@@ -364,7 +364,7 @@ describe('Simple solang tests', function () {
     });
 
     it('account storage too small dynamic realloc', async function () {
-        const { program, storage } = await loadContract('store', [], 233);
+        const { program, storage } = await loadContractAndCallConstructor('store', [], 233);
 
         async function push_until_bang() {
             for (let i = 0; i < 100; i++) {
@@ -379,7 +379,7 @@ describe('Simple solang tests', function () {
     });
 
     it('arrays in account storage', async function () {
-        const { program, storage } = await loadContract('arrays', []);
+        const { program, storage } = await loadContractAndCallConstructor('arrays', []);
 
         let users = [];
 

+ 2 - 2
integration/solana/simple_collectible.spec.ts

@@ -3,7 +3,7 @@
 // DISCLAIMER: This file is an example of how to mint and transfer NFTs on Solana. It is not production ready and has not been audited for security.
 // Use it at your own risk.
 
-import { loadContract, newConnectionAndPayer } from "./setup";
+import { loadContractAndCallConstructor, newConnectionAndPayer } from "./setup";
 import { Keypair } from "@solana/web3.js";
 import { createMint, getOrCreateAssociatedTokenAccount, TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import expect from "expect";
@@ -38,7 +38,7 @@ describe('Simple collectible', function () {
         );
 
         // Each contract in this example is a unique NFT
-        const { provider, program, storage } = await loadContract('SimpleCollectible', [mint, metadata_authority.publicKey]);
+        const { provider, program, storage } = await loadContractAndCallConstructor('SimpleCollectible', [mint, metadata_authority.publicKey]);
 
         const nft_uri = "www.nft.com";
 

+ 11 - 11
integration/solana/system_instruction.spec.ts

@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0
 
-import { loadContract } from "./setup";
+import { loadContractAndCallConstructor } from "./setup";
 import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
 import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
 import { BN } from '@coral-xyz/anchor';
@@ -14,7 +14,7 @@ describe('Test system instructions', function () {
     const seed = 'my_seed_is_tea';
 
     it('create account', async function create_account() {
-        const { program, storage, payer } = await loadContract('TestingInstruction');
+        const { program, storage, payer } = await loadContractAndCallConstructor('TestingInstruction');
         const to_key_pair = Keypair.generate();
 
         await program.methods.createAccount(
@@ -33,7 +33,7 @@ describe('Test system instructions', function () {
     });
 
     it('create account with seed', async function create_account_with_seed() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const base_keypair = Keypair.generate();
         const to_key_pair = await PublicKey.createWithSeed(base_keypair.publicKey, seed, TOKEN_PROGRAM_ID);
 
@@ -56,7 +56,7 @@ describe('Test system instructions', function () {
     });
 
     it('assign', async function assign() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const to_key_pair = Keypair.generate();
 
         const assign_account = new PublicKey('AddressLookupTab1e1111111111111111111111111');
@@ -72,7 +72,7 @@ describe('Test system instructions', function () {
     });
 
     it('assign with seed', async function assign_with_with_seed() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const assign_account = new PublicKey('AddressLookupTab1e1111111111111111111111111');
         const to_key_pair = await PublicKey.createWithSeed(payer.publicKey, seed, assign_account);
 
@@ -91,7 +91,7 @@ describe('Test system instructions', function () {
     });
 
     it('transfer', async function transfer() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const dest = new Keypair();
 
         await program.methods.transfer(
@@ -107,7 +107,7 @@ describe('Test system instructions', function () {
     });
 
     it('transfer with seed', async function transfer_with_seed() {
-        const { storage, payer, provider, program } = await loadContract('TestingInstruction');
+        const { storage, payer, provider, program } = await loadContractAndCallConstructor('TestingInstruction');
         const dest = new Keypair();
         const assign_account = new PublicKey('AddressLookupTab1e1111111111111111111111111');
         const derived_payer = await PublicKey.createWithSeed(payer.publicKey, seed, assign_account);
@@ -133,7 +133,7 @@ describe('Test system instructions', function () {
     });
 
     it('allocate', async function allocate() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const account = Keypair.generate();
 
         await program.methods.allocate(
@@ -147,7 +147,7 @@ describe('Test system instructions', function () {
     });
 
     it('allocate with seed', async function allocate_with_seed() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const account = Keypair.generate();
         const owner = new PublicKey('Stake11111111111111111111111111111111111111');
         const derived_key = await PublicKey.createWithSeed(account.publicKey, seed, owner);
@@ -168,7 +168,7 @@ describe('Test system instructions', function () {
     });
 
     it('create nonce account with seed', async function create_nonce_account_with_seed() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const base_address = Keypair.generate();
         const derived_account = await PublicKey.createWithSeed(base_address.publicKey, seed, system_account);
         const authority = Keypair.generate();
@@ -192,7 +192,7 @@ describe('Test system instructions', function () {
     });
 
     it('nonce accounts', async function nonce_accounts() {
-        const { storage, payer, program } = await loadContract('TestingInstruction');
+        const { storage, payer, program } = await loadContractAndCallConstructor('TestingInstruction');
         const nonce = Keypair.generate();
         const authority = Keypair.generate();
 

+ 2 - 2
integration/solana/token.spec.ts

@@ -2,7 +2,7 @@
 
 import { getOrCreateAssociatedTokenAccount, createMint, TOKEN_PROGRAM_ID } from '@solana/spl-token';
 import { Keypair } from '@solana/web3.js';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import { BN } from '@coral-xyz/anchor';
 import expect from 'expect';
 
@@ -10,7 +10,7 @@ describe('Create spl-token and use from solidity', function () {
     this.timeout(500000);
 
     it('spl-token', async function name() {
-        const { provider, storage, payer, program } = await loadContract('Token');
+        const { provider, storage, payer, program } = await loadContractAndCallConstructor('Token');
         const connection = provider.connection;
 
         const mintAuthority = Keypair.generate();

+ 66 - 0
integration/solana/user_stats.spec.ts

@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Apache-2.0
+
+import {loadContract} from "./setup";
+import {Keypair, PublicKey} from "@solana/web3.js";
+import { utils } from '@coral-xyz/anchor';
+import expect from "expect";
+
+describe('PDA hash table', function() {
+    // A PDA (Program derived address) hash table is a way to store values for a provided key
+    // on a unique account on chain, resembling a hash table. This is an example for achieving
+    // so with Solidity.
+
+    it('Table functions', async function test_table() {
+        const {program, payer} = await loadContract("UserStats");
+        // A user's public key will be the key for the hash table in this example.
+        const myUser = Keypair.generate();
+
+        // The actual 'hash' for our hash table is PDA. We utilize `findProgramAddress`, using the user's
+        // public key as a seed and a 'user-stats' as another seed for randomness. This function will
+        // return the same bump and PDA if the seeds and the program id are the same.
+        const [userStatsPDA, bump] = PublicKey.findProgramAddressSync(
+            [
+                utils.bytes.utf8.encode('user-stats'),
+                myUser.publicKey.toBuffer(),
+            ],
+            program.programId
+        );
+
+        // We create the account to hold the user's related information. The generated PDA becomes the
+        // data account for our contract.
+        // If a contract for `userStatsPDA` already exists, this function will fail.
+        await program.methods.new(myUser.publicKey.toBuffer(), bump, "user-one", 25)
+            .accounts({
+                    dataAccount: userStatsPDA,
+                    wallet: payer.publicKey,
+            })
+            .signers([payer])
+            .rpc();
+
+        // To read the information from the contract, the data account is also necessary
+        // If there is no contract created for `userStatsPDA`, this function will fail.
+        let res = await program.methods.returnStats()
+            .accounts({ dataAccount: userStatsPDA })
+            .view();
+
+        expect(res.return0).toBe("user-one");
+        expect(res.return1).toBe(25);
+        expect(res.return2).toBe(bump);
+
+        // These function update the information in the contract.
+        // If there is no contract created for `userStatsPDA`, these calls will fail.
+        await program.methods.changeUserName("new-user-one")
+            .accounts({ dataAccount: userStatsPDA })
+            .rpc();
+        await program.methods.changeLevel(20)
+            .accounts({ dataAccount: userStatsPDA })
+            .rpc();
+        res = await program.methods.returnStats()
+            .accounts({ dataAccount: userStatsPDA })
+            .view();
+
+        expect(res.return0).toBe("new-user-one");
+        expect(res.return1).toBe(20);
+        expect(res.return2).toBe(bump);
+    });
+});

+ 2 - 2
integration/solana/verify_sig.spec.ts

@@ -3,7 +3,7 @@
 import { Keypair, Ed25519Program, SYSVAR_INSTRUCTIONS_PUBKEY, PublicKey } from '@solana/web3.js';
 import expect from 'expect';
 import nacl from 'tweetnacl';
-import { loadContract } from './setup';
+import { loadContractAndCallConstructor } from './setup';
 import { Program } from '@coral-xyz/anchor';
 
 describe('Signature Check', function () {
@@ -14,7 +14,7 @@ describe('Signature Check', function () {
     let payer: Keypair;
 
     before(async function () {
-        ({ program, storage, payer } = await loadContract('verify_sig'));
+        ({ program, storage, payer } = await loadContractAndCallConstructor('verify_sig'));
     });
 
     it('check valid signature', async function () {