Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add seeds::program constraint for PDAs. #1197

Merged
merged 19 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ incremented for features.

## [Unreleased]

#### Fixes
### Fixes

### Features

*lang: Improved error msgs when required programs are missing when using the `init` constraint([#1257](https://github.com/project-serum/anchor/pull/1257))
Copy link
Contributor

@paul-schaaf paul-schaaf Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used the opportunity to move this into feature section (wasnt really a bug fix)

*lang: Add seeds::program constraint for specifying which program_id to use when deriving PDAs.([#1197](https://github.com/project-serum/anchor/pull/1197))

## [0.20.0] - 2022-01-06

Expand Down
15 changes: 12 additions & 3 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2::TokenStream {
let name = &f.ident;
let s = &mut c.seeds.clone();

let deriving_program_id = c
.program_seed
.clone()
// If they specified a program_seed to use when deriving the PDA, use it
.map(|program_id| quote! { #program_id })
// otherwise fall back to the current program's program_id
armaniferrante marked this conversation as resolved.
Show resolved Hide resolved
.unwrap_or(quote! { program_id });

// If the seeds came with a trailing comma, we need to chop it off
// before we interpolate them below.
if let Some(pair) = s.pop() {
Expand All @@ -340,7 +349,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
quote! {
let (__program_signer, __bump) = anchor_lang::solana_program::pubkey::Pubkey::find_program_address(
&[#s],
program_id,
&#deriving_program_id,
);
if #name.key() != __program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
Expand All @@ -362,7 +371,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
&[
Pubkey::find_program_address(
&[#s],
program_id,
&#deriving_program_id,
).1
][..]
]
Expand All @@ -378,7 +387,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
quote! {
let __program_signer = Pubkey::create_program_address(
&#seeds[..],
program_id,
&#deriving_program_id,
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
if #name.key() != __program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
Expand Down
9 changes: 8 additions & 1 deletion lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ pub enum ConstraintToken {
MintFreezeAuthority(Context<ConstraintMintFreezeAuthority>),
MintDecimals(Context<ConstraintMintDecimals>),
Bump(Context<ConstraintTokenBump>),
ProgramSeed(Context<ConstraintTokenProgramSeed>),
}

impl Parse for ConstraintToken {
Expand Down Expand Up @@ -687,7 +688,8 @@ pub struct ConstraintInitGroup {
pub struct ConstraintSeedsGroup {
pub is_init: bool,
pub seeds: Punctuated<Expr, Token![,]>,
pub bump: Option<Expr>, // None => bump was given without a target.
pub bump: Option<Expr>, // None => bump was given without a target.
pub program_seed: Option<Expr>, // None => use the current program's program_id
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -771,6 +773,11 @@ pub struct ConstraintTokenBump {
bump: Option<Expr>,
}

#[derive(Debug, Clone)]
pub struct ConstraintTokenProgramSeed {
program_seed: Expr,
}

#[derive(Debug, Clone)]
pub struct ConstraintAssociatedToken {
pub wallet: Expr,
Expand Down
41 changes: 41 additions & 0 deletions lang/syn/src/parser/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
};
ConstraintToken::Bump(Context::new(ident.span(), ConstraintTokenBump { bump }))
}
"program_seed" => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any place in the existing solana sdk that uses the term "program_seed"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag @jstarry. Any ideas for an accurate keyword to refer to the base program id for PDAs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah tbh I also find that term a little odd

Copy link
Contributor

@paul-schaaf paul-schaaf Jan 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other words mentioned in discord: seeds::program, program, derive_from, seed_program

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. Let's do seeds::program.

stream.parse::<Token![=]>()?;
ConstraintToken::ProgramSeed(Context::new(
ident.span(),
ConstraintTokenProgramSeed {
program_seed: stream.parse()?,
},
))
}
_ => {
stream.parse::<Token![=]>()?;
let span = ident
Expand Down Expand Up @@ -308,6 +317,7 @@ pub struct ConstraintGroupBuilder<'ty> {
pub mint_freeze_authority: Option<Context<ConstraintMintFreezeAuthority>>,
pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
pub bump: Option<Context<ConstraintTokenBump>>,
pub program_seed: Option<Context<ConstraintTokenProgramSeed>>,
}

impl<'ty> ConstraintGroupBuilder<'ty> {
Expand Down Expand Up @@ -338,6 +348,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_freeze_authority: None,
mint_decimals: None,
bump: None,
program_seed: None,
}
}

Expand Down Expand Up @@ -494,6 +505,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_freeze_authority,
mint_decimals,
bump,
program_seed,
} = self;

// Converts Option<Context<T>> -> Option<T>.
Expand All @@ -519,6 +531,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
bump: into_inner!(bump)
.map(|b| b.bump)
.expect("bump must be provided with seeds"),
program_seed: into_inner!(program_seed).map(|id| id.program_seed),
});
let associated_token = match (associated_token_mint, associated_token_authority) {
(Some(mint), Some(auth)) => Some(ConstraintAssociatedToken {
Expand Down Expand Up @@ -620,6 +633,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::MintFreezeAuthority(c) => self.add_mint_freeze_authority(c),
ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c),
ConstraintToken::Bump(c) => self.add_bump(c),
ConstraintToken::ProgramSeed(c) => self.add_program_seed(c),
}
}

Expand Down Expand Up @@ -725,6 +739,33 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}

fn add_program_seed(&mut self, c: Context<ConstraintTokenProgramSeed>) -> ParseResult<()> {
if self.program_seed.is_some() {
return Err(ParseError::new(c.span(), "program_seed already provided"));
}
if self.seeds.is_none() {
return Err(ParseError::new(
c.span(),
"seeds must be provided before program_seed",
));
}
if let Some(ref init) = self.init {
if init.if_needed {
return Err(ParseError::new(
c.span(),
"program_seed cannot be used with init_if_needed",
));
} else {
return Err(ParseError::new(
c.span(),
"program_seed cannot be used with init",
));
}
}
self.program_seed.replace(c);
Ok(())
}

fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
if self.token_authority.is_some() {
return Err(ParseError::new(
Expand Down
3 changes: 3 additions & 0 deletions tests/misc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
},
"scripts": {
"test": "anchor test"
},
"dependencies": {
"mocha": "^9.1.3"
}
}
8 changes: 8 additions & 0 deletions tests/misc/programs/misc/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,11 @@ pub struct InitIfNeededChecksRentExemption<'info> {
pub system_program: Program<'info, System>
}

#[instruction(bump: u8)]
pub struct TestProgramIdConstraint<'info> {
armaniferrante marked this conversation as resolved.
Show resolved Hide resolved
#[account(seeds = [b"seed"], bump, program_seed = anchor_spl::associated_token::ID)]
associated_token_account: AccountInfo<'info>,

#[account(seeds = [b"seed"], bump, program_seed = crate::ID)]
other_account: AccountInfo<'info>,
}
5 changes: 5 additions & 0 deletions tests/misc/programs/misc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ pub mod misc {
}

pub fn init_if_needed_checks_rent_exemption(_ctx: Context<InitIfNeededChecksRentExemption>) -> ProgramResult {

pub fn test_program_id_constraint(
_ctx: Context<TestProgramIdConstraint>,
_bump: u8,
) -> ProgramResult {
Ok(())
}
}
48 changes: 48 additions & 0 deletions tests/misc/tests/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -1528,4 +1528,52 @@ describe("misc", () => {
assert.equal("A rent exempt constraint was violated", err.msg);
}
});

it("Can validate PDAs derived from other program ids", async () => {
const [ourPda, ourPdaBump] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("seed")],
program.programId
);
const wrongAddress = anchor.web3.Keypair.generate().publicKey;
try {
await program.rpc.testProgramIdConstraint(123, {
accounts: {
associatedTokenAccount: wrongAddress,
otherAccount: ourPda,
},
});
assert.ok(false);
} catch (err) {
assert.equal(err.code, 2006);
}

const [wrongProgramIdPDA, wrongBump] =
await anchor.web3.PublicKey.findProgramAddress(
["seed"],
program.programId
);
try {
await program.rpc.testProgramIdConstraint(wrongBump, {
accounts: {
associatedTokenAccount: wrongProgramIdPDA,
otherAccount: ourPda,
},
});
assert.ok(false);
} catch (err) {
assert.equal(err.code, 2006);
}

const [rightProgramIdPDA, rightBump] =
await anchor.web3.PublicKey.findProgramAddress(
["seed"],
ASSOCIATED_TOKEN_PROGRAM_ID
);
await program.rpc.testProgramIdConstraint(rightBump, {
accounts: {
associatedTokenAccount: rightProgramIdPDA,
otherAccount: ourPda,
},
});
});
});