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
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
@@ -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

15 changes: 12 additions & 3 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
@@ -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() {
@@ -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());
@@ -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
][..]
]
@@ -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());
9 changes: 8 additions & 1 deletion lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -610,6 +610,7 @@ pub enum ConstraintToken {
MintFreezeAuthority(Context<ConstraintMintFreezeAuthority>),
MintDecimals(Context<ConstraintMintDecimals>),
Bump(Context<ConstraintTokenBump>),
ProgramSeed(Context<ConstraintTokenProgramSeed>),
}

impl Parse for ConstraintToken {
@@ -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)]
@@ -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,
41 changes: 41 additions & 0 deletions lang/syn/src/parser/accounts/constraints.rs
Original file line number Diff line number Diff line change
@@ -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
@@ -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> {
@@ -338,6 +348,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_freeze_authority: None,
mint_decimals: None,
bump: None,
program_seed: None,
}
}

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

// Converts Option<Context<T>> -> Option<T>.
@@ -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 {
@@ -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),
}
}

@@ -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(
3 changes: 3 additions & 0 deletions tests/misc/package.json
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
@@ -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,
},
});
});
});