3

When passing signer seeds to a CPI like this:

    let seeds = &[
        b"whatever".as_ref(),
        &ctx.accounts.someacc.key().to_bytes()[..],
        &[bump]
    ];
    let signers_seeds = &[&seeds[..]];

    invoke_signed(&instruction, &account_infos, signers_seeds)?;

Noting that the bump is also passed in the instruction args

#[derive(AnchorSerialize, AnchorDeserialize)]
struct CpiArgs {
    bump: u8,
}

fn get_ix_data(bump: u8) -> Vec<u8> {
    let hash = get_function_hash("global", "init_account");
    let mut buf: Vec<u8> = vec![];
    buf.extend_from_slice(&hash);
    let args = CpiArgs { bump };
    args.serialize(&mut buf).unwrap();
    buf
}

With checks on the receiving end of the CPI (the ix this CPI is calling) like this:

#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct InitializeAccount<'info> {
    #[account(init,
        seeds = [
            b"whatever".as_ref(),
            someacc.key().as_ref(),
        ],
        bump,
        token::mint = some_mint,
        token::authority = some_auth,
        payer = owner)]
    pub some_ata: Account<'info, TokenAccount>,
// someacc and others...
}

I noticed that for some key combinations, this will fail with Could not create program address with signer seeds: Provided seeds do not result in a valid address. Removing the &[bump] fixes it for those cases, but breaks it for others!

It seems that the rule is, if bump = 254, omit it. For example this silly fix DOES work for both 254 and 255 bumps (though it still fails for bumps < 254):

    if bump == 254 {
        let seeds = &[
            b"whatever".as_ref(),
            &ctx.accounts.someata.key().to_bytes()[..],
            //&[bump]
        ];
        let signers_seeds = &[&seeds[..]];
        invoke_signed(&instruction, &account_infos, signers_seeds)?;
    } else {
         let seeds = &[
            b"whatever".as_ref(),
            &ctx.accounts.someacc.key().to_bytes()[..],
            &[bump]
        ];
        let signers_seeds = &[&seeds[..]];
        invoke_signed(&instruction, &account_infos, signers_seeds)?;
    }

But intuitively, the first branch shouldn't work at all, as the bump is missing.

5
  • yes, if bump is a u8, then &[bump] should always be the last item in the seeds array.
    – m_callens
    Feb 20, 2023 at 18:30
  • For some key combinations, removing the &[bump] from the signer seeds resolves the Could not create program address error. In at least one of those cases bump was 245, which is a valid u8, so why?
    – Whiteseal
    Feb 20, 2023 at 18:37
  • then the other data being provided is not resulting in the same calculated bump nonce that you are providing. for pubkeys you can just do ctx.accounts.someacc.key().as_ref() and constants, just "whatever".as_ref(). just make sure that the bump nonce you are adding to the ends is the right one for the pda you are providing the seeds for.
    – m_callens
    Feb 20, 2023 at 20:10
  • If the front end gets the bump with [key, bump] = PublicKey.findProgramAddressSync(seeds, id) then passes it to the ix as an arg e.g. program.methods.someix(bump).accounts({accounts}).instruction() then shouldn't that be the correct bump? Again it works if the bump is removed from the seed args, for this particular key combination. What is the signers_seeds using for the bump when I omit it?
    – Whiteseal
    Feb 21, 2023 at 1:59
  • you can also get the bump from the ctx.bumps map, e.g., if you have a pda in your accounts struct with seeds defined there, if you need to get the bump in the instruction, you can get from ctx.bumps[<account_struct_prop_name>]. then you don't have to calc and pass in from the front end. Feb 21, 2023 at 23:35

1 Answer 1

3
+25

For the same combination of (optional) input seeds and a program_id, there are multiple bump seeds that can generate valid PDAs (which will have different addresses). When you use find_program_address to generate a PDA, it will always return the "canonical bump" (the first bump found that generates a valid PDA).

find_program_address finds a valid PDA by adding a bump seed (starting from 255) to the list of optional seeds you provide and the program ID, and then calls the create_program_address function until a valid PDA is found (retrying with a new bump iterating down from 255). Once found, both the PDA and the bump are returned.

Under the hood, here's what find_program_address does (code below is simplified)

https://github.com/solana-labs/solana/blob/master/sdk/program/src/pubkey.rs#L476

 pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
    Self::try_find_program_address(seeds, program_id)
        .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"))
}

find_program_address calls try_find_program_address, where a bump (starting from 255) is added and create_program_address is called in a loop

https://github.com/solana-labs/solana/blob/master/sdk/program/src/pubkey.rs#L494

pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> {

    let mut bump_seed = [std::u8::MAX];
    for _ in 0..std::u8::MAX {
        {
            let mut seeds_with_bump = seeds.to_vec();
            seeds_with_bump.push(&bump_seed);
            match Self::create_program_address(&seeds_with_bump, program_id) {
                Ok(address) => return Some((address, bump_seed[0])),
                Err(PubkeyError::InvalidSeeds) => (),
                _ => break,
            }
        }
        bump_seed[0] -= 1;
    }
    None

}

If create_program_address generates a valid PDA, the PDA is returned, otherwise the bump seed is decremented by 1 and create_program_address is called again with the new bump seed.

https://github.com/solana-labs/solana/blob/master/sdk/program/src/pubkey.rs#L577

pub fn create_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
) -> Result<Pubkey, PubkeyError> {

    let mut hasher = crate::hash::Hasher::default();
    for seed in seeds.iter() {
        hasher.hash(seed);
    }
    hasher.hashv(&[program_id.as_ref(), PDA_MARKER]);
    let hash = hasher.result();

    if bytes_are_curve_point(hash) {
        return Err(PubkeyError::InvalidSeeds);
    }

    Ok(Pubkey::new(hash.as_ref()))

} 

When you use invoke_signed, you are providing the list of signer_seeds that should generate the valid PDA you need to sign for your CPI.

https://docs.rs/solana-program/1.15.2/solana_program/pubkey/struct.Pubkey.html#method.find_program_address

Program derived addresses (PDAs) are account keys that only the program, program_id, has the authority to sign. The address is of the same form as a Solana Pubkey, except they are ensured to not be on the ed25519 curve and thus have no associated private key. When performing cross-program invocations the program can “sign” for the key by calling invoke_signed and passing the same seeds used to generate the address, along with the calculated bump seed, which this function returns as the second tuple element. The runtime will verify that the program associated with this address is the caller and thus authorized to be the signer.

Because the program address must not lie on the ed25519 curve, there may be seed and program id combinations that are invalid. For this reason, an extra seed (the bump seed) is calculated that results in a point off the curve. The bump seed must be passed as an additional seed when calling invoke_signed.

It could be just by chance that when you exclude the bump seed in your example above, that a valid PDA is still being generated. Depending on how you set up your instruction and whether you included checks to validate the PDA/bump, it could explain why your CPI is still going through.

Here's a great article that goes over how PDAs and bumps work: https://www.sec3.dev/blog/pda-bump-seeds

5
  • The naming here is interesting, because in try_find_program_address it's called seeds_with_bump only AFTER the bump is appended. That seems to imply it wants seeds without a bump as input. In Solana's docs elsewhere, they also don't show the bump being passed: docs.solana.com/developing/programming-model/…. So it seems canonically excluding the &[bump] is the correct answer. It happens too often to be by chance, and the receiver checks the seeds + bump in its init constraints.
    – Whiteseal
    Feb 23, 2023 at 16:59
  • You should not exclude the bump. There's about a 50/50 chance a valid PDA is generated. The bump is what's used to ensure a valid PDA is found. I don't think invoke_signed calls find_program_address, that's just for background info on PDAs.
    – john
    Feb 23, 2023 at 17:16
  • If the wrong seeds were supplied, it would generate a different valid PDA, since the hash would be different. On the front end, PublicKey.findProgramAddressSync(...) returns key= some_key, bump=254. That key/bump are sent to the program, which sends it to the CPI. The bump being omitted in signer seeds should end up deriving the wrong address, so how/why does it still generate some_key exactly? In the same case, adding the bump back to signer seeds causes it to fail to find a valid address. It may be related to github.com/solana-labs/solana/issues/21409
    – Whiteseal
    Feb 23, 2023 at 20:25
  • Also, the result of PublicKey.findProgramAddressSync(...) is the same key/bump as anchor_lang::prelude::Pubkey::find_program_address on chain. So the bump that is passed or omitted from the signers_seeds is definitively the canonical valid bump for that key.
    – Whiteseal
    Feb 23, 2023 at 20:41
  • What is the instruction that signers_seeds is required as a signer for? (which appear to be the seeds for the PDA of the TokenAccount) It looks like you've used init to create the TokenAccount (which means you don't need to manually add an instruction to create the TokenAccount) and the auth for the TokenAccount is some_auth.
    – john
    Mar 2, 2023 at 23:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.