How to derive PDAs with multiple seeds in Anchor / Rust?

1.7k views Asked by At

I am trying to create a structure where every user can generate up to 255 PDA's to store data in. I do this by passing a fixed string, the user's wallet Pubkey and an ID (as a u8). However, I always get a Cross-program invocation with unauthorized signer or writable account error; it says that "689GkVL9FauDTVVV6hbW3vWeLa7cBzAHk1Y1sxEFhCqR's signer privilege escalated".

I have a working system for deriving PDA's using only the wallet and a fixed seed string, but that of course only allows for a single PDA per user.

Here's my Anchor code:

#[program]
pub mod pda_test {
    use super::*;

    pub fn maak_bedrijf(ctx: Context<MaakBedrijf>, naam: String, omschrijving: String, url: String, geverifieerd: bool, bump: u8, id: u8) -> Result<()> {
        let bedrijf = &mut ctx.accounts.bedrijf;

        require!(naam.len()         < MAX_LENGTE_NAAM,         BedrijfError::BedrijfsnaamTeLang);
        require!(omschrijving.len() < MAX_LENGTE_OMSCHRIJVING, BedrijfError::OmschrijvingTeLang);
        require!(url.len()          < MAX_LENGTE_URL,          BedrijfError::UrlTeLang);

        bedrijf.bump     = bump;
        bedrijf.eigenaar = ctx.accounts.gebruiker.key();
        
        bedrijf.naam         = naam;
        bedrijf.omschrijving = omschrijving;
        bedrijf.url          = url;
        bedrijf.geverifieerd = geverifieerd;

        bedrijf.id = id;
        
        Ok(())
    }
}
#[account]
pub struct Bedrijf {
    bump: u8,
    eigenaar: Pubkey,

    naam:         String,
    omschrijving: String,
    url:          String,
    geverifieerd: bool,

    id: u8
}

impl Bedrijf {
    pub fn ruimte() -> usize {
        32 + // een Pubkey is 32 bytes lang
        1  + // de bump is een u8

        4 + MAX_LENGTE_NAAM +         // 4 bytes string discriminator + max lengte v/d string
        4 + MAX_LENGTE_OMSCHRIJVING + // ditto
        4 + MAX_LENGTE_URL +          // ditto
        1 +   // een bool wordt weergegeven als een u8, dus ook één byte (zie ook: https://github.com/near/borsh)
        1     // de id is ook een u8
    }
}

#[derive(Accounts)]
#[instruction(id: u8)]
pub struct MaakBedrijf<'info> {
    #[account(mut)]
    pub gebruiker: Signer<'info>,

    #[account(
        init,
        payer = gebruiker,
        space = 8 + Bedrijf::ruimte(), 
        seeds = [b"bedrijfpda".as_ref(), &[id], gebruiker.key().as_ref()],
        bump
    )]
    pub bedrijf: Account<'info, Bedrijf>,
    pub system_program: Program<'info, System>,
}

And here's how the code looks in my TypeScript client:

const [bedrijfPDA, bump] = await PublicKey.findProgramAddress(
      [
        anchor.utils.bytes.utf8.encode("bedrijfpda"), // hiervoor hebben we deze seed nodig
        Uint8Array.of(id),
        wallet.publicKey.toBytes(),                   // en de publicKey van de wallet
      ],
      program.programId // we leiden af vanaf ons huidige programma
    );

const tx = program.methods.maakBedrijf( // maakBedrijf is de belangrijkste methode.
      data.naam,
      data.omschrijving,
      data.url,
      data.geverifieerd,
      bump, // de bump die we vonden is nodig om ook door te geven aan het interne account  
      id
  ).signers([payer]).accounts( // we geven de wallet door als een signer
    {
      bedrijf:       bedrijfPDA,
      gebruiker:     payer.publicKey,
      systemProgram: SystemProgram.programId
    }
  );
let werkende_transactie: anchor.web3.Transaction = undefined;
  
  await tx.transaction().then(
    async (transactie) => {
      // van zodra dat we de functie uitgevoerd hebben krijgen we een Transaction-object...
      let recenteHash = await connection.getLatestBlockhash() // die nog een recente blockhash nodig heeft

      transactie.recentBlockhash = recenteHash.blockhash
      transactie.feePayer        = payer.publicKey
      
      transactie.sign(payer) // ook word de transactie daadwerkelijk gesignd

      werkende_transactie = transactie; // en we kennen de waarde toe aan de externe variabele
      //console.log("Succes: ", transactie);
    },
    (reden) => {console.log("Foutmelding: ", reden)}
  );
await anchor.web3.sendAndConfirmTransaction(
    connection,
    werkende_transactie,
    [payer],
    {commitment: "confirmed"}
  ).then(
    (str) => {
      console.log(str)
    },
    (reden) => {console.log("Foutmelding: ", reden)}
  );

The full error log and transaction details are as follows:

ransaction {
  signatures: [
    {
      signature: <Buffer f2 eb fe ca 6c e0 f6 33 22 de 69 7d 81 2c c7 a8 98 2b 94 f6 08 e6 15 fb ae fb 49 07 5a b5 be 77 6e 2f 90 2e 6e ed 88 31 ed 2a 94 23 be 93 0a 67 a4 fc ... 14 more bytes>,
      publicKey: [PublicKey]
    }
  ],
  feePayer: PublicKey {
    _bn: <BN: 5f9dc00f214bba9749dc42cfde23e4ebcf76a0f70901c748897c2b65c70f845e>
  },
  instructions: [
    TransactionInstruction {
      keys: [Array],
      programId: [PublicKey],
      data: <Buffer a5 2e 9e e5 9a 97 29 b9 0d 00 00 00 53 6e 61 63 6b 62 61 72 20 54 6f 6e 79 23 00 00 00 44 65 20 62 65 73 74 65 20 73 6e 61 63 6b 73 20 76 61 6e 20 68 ... 48 more bytes>
    }
  ],
  recentBlockhash: 'ERRTheoc8z1zcRzu5zfGCMUVEgjL8NsLXst6PaY7yFfw',
  nonceInfo: undefined
}
Foutmelding:  SendTransactionError: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: Cross-program invocation with unauthorized signer or writable account
    at Connection.sendEncodedTransaction (/home/simeon/dev/rust/anchor/pda_test/node_modules/@solana/web3.js/lib/index.cjs.js:6929:13)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async Connection.sendRawTransaction (/home/simeon/dev/rust/anchor/pda_test/node_modules/@solana/web3.js/lib/index.cjs.js:6890:20)
    at async Connection.sendTransaction (/home/simeon/dev/rust/anchor/pda_test/node_modules/@solana/web3.js/lib/index.cjs.js:6880:12)
    at async Object.sendAndConfirmTransaction (/home/simeon/dev/rust/anchor/pda_test/node_modules/@solana/web3.js/lib/index.cjs.js:3027:21) {
  logs: [
    'Program A9RfydetpSqT5rLRZNmeVaeyqG84tXm8cNZkn6Zy1817 invoke [1]',
    'Program log: Instruction: MaakBedrijf',
    "689GkVL9FauDTVVV6hbW3vWeLa7cBzAHk1Y1sxEFhCqR's signer privilege escalated",
    'Program A9RfydetpSqT5rLRZNmeVaeyqG84tXm8cNZkn6Zy1817 consumed 15025 of 200000 compute units',
    'Program A9RfydetpSqT5rLRZNmeVaeyqG84tXm8cNZkn6Zy1817 failed: Cross-program invocation with unauthorized signer or writable account'
  ]
}
3

There are 3 answers

1
Bozhidar On

I had the same problem. In your TypeScript client replace Uint8Array.of(id) with [id].

0
siong1987 On

The problem is on #[instruction(id: u8)], if you look at the documentation for the instruction macro: https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html#instruction-attribute

It mentions that:

You have to list them in the same order as in the instruction but you can omit all arguments after the last one you need.

So, your id here is definitely not the same order as in your instruction. So, to make this work, you can do:

pub fn maak_bedrijf(ctx: Context<MaakBedrijf>, id: u8, naam: String, omschrijving: String, url: String, geverifieerd: bool, bump: u8) -> Result<()> {

Basically, move id: u8 to be the first parameter in your instruction method.

0
Anoushk On

Anchor has an in built utility for this on the client side

new anchor.BN(0).toArrayLike(Buffer)

On the rust side you can use this in your seeds array

 seeds = [b"seed", &[0 as u8]],

important part being &[0 as u8]