diff --git a/.github/workflows/reusable-tests.yaml b/.github/workflows/reusable-tests.yaml index b818919dbc..f7c08e2948 100644 --- a/.github/workflows/reusable-tests.yaml +++ b/.github/workflows/reusable-tests.yaml @@ -185,7 +185,7 @@ jobs: path: ~/.cargo/bin/ - run: chmod +rwx ~/.cargo/bin/anchor - - run: cd ${{ matrix.node.path }} && anchor build --skip-lint + - run: cd ${{ matrix.node.path }} && anchor build --skip-lint --ignore-keys - uses: actions/upload-artifact@v4 with: name: ${{ matrix.node.name }} @@ -276,7 +276,7 @@ jobs: name: start validator - run: cd tests/bpf-upgradeable-state && yarn --frozen-lockfile - run: cd tests/bpf-upgradeable-state - - run: cd tests/bpf-upgradeable-state && anchor build --skip-lint + - run: cd tests/bpf-upgradeable-state && anchor build --skip-lint --ignore-keys - run: cd tests/bpf-upgradeable-state && solana program deploy --program-id program_with_different_programdata.json target/deploy/bpf_upgradeable_state.so - run: cd tests/bpf-upgradeable-state && cp bpf_upgradeable_state-keypair.json target/deploy/bpf_upgradeable_state-keypair.json && anchor test --skip-local-validator --skip-build --skip-lint - run: cd tests/bpf-upgradeable-state && npx tsc --noEmit @@ -438,7 +438,7 @@ jobs: path: tests/safety-checks - cmd: cd tests/custom-coder && anchor test --skip-lint && npx tsc --noEmit path: tests/custom-coder - - cmd: cd tests/custom-discriminator && anchor test + - cmd: cd tests/custom-discriminator && anchor test --skip-lint && npx tsc --noEmit path: tests/custom-discriminator - cmd: cd tests/validator-clone && anchor test --skip-lint && npx tsc --noEmit path: tests/validator-clone @@ -462,7 +462,7 @@ jobs: path: tests/bench - cmd: cd tests/idl && ./test.sh path: tests/idl - - cmd: cd tests/lazy-account && anchor test + - cmd: cd tests/lazy-account && anchor test --skip-lint path: tests/lazy-account - cmd: cd tests/test-instruction-validation && ./test.sh path: tests/test-instruction-validation diff --git a/CHANGELOG.md b/CHANGELOG.md index 438d8d8b7d..45c9cfc93d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The minor version will be incremented upon a breaking change and the patch versi ### Features +- cli: Added a `check_program_id_mismatch` in build time to check if the program ID in the source code matches the program ID in the keypair file ([#4018](https://github.com/solana-foundation/anchor/pull/4018)). This check will be skipped during `anchor test`. + ### Fixes - idl: Fix defined types with unsupported fields not producing an error ([#4088](https://github.com/solana-foundation/anchor/pull/4088)). diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 79b54cdc47..bcc971d2a0 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -98,6 +98,9 @@ pub enum Command { /// True if the build should not fail even if there are no "CHECK" comments #[clap(long)] skip_lint: bool, + /// Skip checking for program ID mismatch between keypair and declare_id + #[clap(long)] + ignore_keys: bool, /// Do not build the IDL #[clap(long)] no_idl: bool, @@ -316,6 +319,9 @@ pub enum Command { /// no "CHECK" comments where normally required #[clap(long)] skip_lint: bool, + /// Skip checking for program ID mismatch between keypair and declare_id + #[clap(long)] + ignore_keys: bool, /// Architecture to use when building the program #[clap(value_enum, long, default_value = "sbf")] arch: ProgramArch, @@ -758,6 +764,7 @@ fn process_command(opts: Opts) -> Result<()> { cargo_args, env, skip_lint, + ignore_keys, no_docs, arch, } => build( @@ -767,6 +774,7 @@ fn process_command(opts: Opts) -> Result<()> { idl_ts, verifiable, skip_lint, + ignore_keys, program_name, solana_version, docker_image, @@ -868,6 +876,7 @@ fn process_command(opts: Opts) -> Result<()> { skip_build, skip_deploy, skip_lint, + ignore_keys, env, cargo_args, arch, @@ -876,6 +885,7 @@ fn process_command(opts: Opts) -> Result<()> { skip_build, skip_deploy, skip_lint, + ignore_keys, env, cargo_args, arch, @@ -1268,6 +1278,7 @@ pub fn build( idl_ts: Option, verifiable: bool, skip_lint: bool, + ignore_keys: bool, program_name: Option, solana_version: Option, docker_image: Option, @@ -1296,6 +1307,11 @@ pub fn build( check_anchor_version(&cfg).ok(); check_deps(&cfg).ok(); + // Check for program ID mismatches before building (skip if --ignore-keys is used), Always skipped in anchor test + if !ignore_keys { + check_program_id_mismatch(&cfg, program_name.clone())?; + } + let idl_out = match idl { Some(idl) => Some(PathBuf::from(idl)), None => Some(cfg_parent.join("target").join("idl")), @@ -2728,6 +2744,7 @@ fn test( None, false, skip_lint, + true, program_name.clone(), None, None, @@ -3424,7 +3441,7 @@ fn deploy( let retry_delay = std::time::Duration::from_millis(500); let cache_delay = std::time::Duration::from_secs(2); - println!("Waiting for program {} to be confirmed...", program_id); + println!("Waiting for program {program_id} to be confirmed..."); for attempt in 0..max_retries { if let Ok(account) = client.get_account(&program_id) { @@ -3851,11 +3868,58 @@ fn keys_sync(cfg_override: &ConfigOverride, program_name: Option) -> Res }) } +/// Check if there's a mismatch between the program keypair and the `declare_id!` in the source code. +/// Returns an error if a mismatch is detected, prompting the user to run `anchor keys sync`. +fn check_program_id_mismatch(cfg: &WithPath, program_name: Option) -> Result<()> { + let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#) + .multi_line(true) + .build() + .unwrap(); + + for program in cfg.get_programs(program_name)? { + // Get the pubkey from the keypair file + let actual_program_id = program.pubkey()?.to_string(); + + // Check declaration in program files + let src_path = program.path.join("src"); + let files_to_check = vec![src_path.join("lib.rs"), src_path.join("id.rs")]; + + for path in files_to_check { + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(_) => continue, + }; + + let incorrect_program_id = declare_id_regex + .captures(&content) + .and_then(|captures| captures.get(3)) + .filter(|program_id_match| program_id_match.as_str() != actual_program_id); + + if let Some(program_id_match) = incorrect_program_id { + let declared_id = program_id_match.as_str(); + return Err(anyhow!( + "Program ID mismatch detected for program '{}':\n \ + Keypair file has: {}\n \ + Source code has: {}\n\n\ + Please run 'anchor keys sync' to update the program ID in your source code or use the '--ignore-keys' flag to skip this check.", + program.lib_name, + actual_program_id, + declared_id + )); + } + } + } + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] fn localnet( cfg_override: &ConfigOverride, skip_build: bool, skip_deploy: bool, skip_lint: bool, + ignore_keys: bool, env_vars: Vec, cargo_args: Vec, arch: ProgramArch, @@ -3870,6 +3934,7 @@ fn localnet( None, false, skip_lint, + ignore_keys, None, None, None, diff --git a/cli/src/rust_template.rs b/cli/src/rust_template.rs index fb8e48c620..ab8e078782 100644 --- a/cli/src/rust_template.rs +++ b/cli/src/rust_template.rs @@ -56,11 +56,10 @@ pub fn create_program(name: &str, template: ProgramTemplate, with_mollusk: bool) fn rust_toolchain_toml() -> String { format!( r#"[toolchain] -channel = "{msrv}" +channel = "{ANCHOR_MSRV}" components = ["rustfmt","clippy"] profile = "minimal" -"#, - msrv = ANCHOR_MSRV +"# ) } @@ -748,15 +747,12 @@ name = "tests" version = "0.1.0" description = "Created with Anchor" edition = "2021" -rust-version = "{msrv}" +rust-version = "{ANCHOR_MSRV}" [dependencies] -anchor-client = "{version}" +anchor-client = "{VERSION}" {name} = {{ version = "0.1.0", path = "../programs/{name}" }} -"#, - msrv = ANCHOR_MSRV, - version = VERSION, - name = name, +"# ) } diff --git a/client/example/run-test.sh b/client/example/run-test.sh index 5aba1a4372..14bd4f9ab1 100755 --- a/client/example/run-test.sh +++ b/client/example/run-test.sh @@ -28,15 +28,15 @@ main() { local events_pid="2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy" local optional_pid="FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG" - cd ../../tests/composite && anchor build && cd - + cd ../../tests/composite && anchor build --skip-lint --ignore-keys && cd - [ $? -ne 0 ] && exit 1 - cd ../../examples/tutorial/basic-2 && anchor build && cd - + cd ../../examples/tutorial/basic-2 && anchor build --skip-lint --ignore-keys && cd - [ $? -ne 0 ] && exit 1 - cd ../../examples/tutorial/basic-4 && anchor build && cd - + cd ../../examples/tutorial/basic-4 && anchor build --skip-lint --ignore-keys && cd - [ $? -ne 0 ] && exit 1 - cd ../../tests/events && anchor build && cd - + cd ../../tests/events && anchor build --skip-lint --ignore-keys && cd - [ $? -ne 0 ] && exit 1 - cd ../../tests/optional && anchor build && cd - + cd ../../tests/optional && anchor build --skip-lint --ignore-keys && cd - [ $? -ne 0 ] && exit 1 # diff --git a/tests/bench/tests/stack-memory.ts b/tests/bench/tests/stack-memory.ts index 33e19c733a..b1da21912c 100644 --- a/tests/bench/tests/stack-memory.ts +++ b/tests/bench/tests/stack-memory.ts @@ -43,7 +43,11 @@ describe("Stack memory", () => { // Expected error: // Error: Function _ZN5bench9__private8__global13account_info117h88e5c10f03de9fddE // Stack offset of 4424 exceeded max offset of 4096 by 328 bytes - const buildResult = spawn("anchor", ["build", "--skip-lint"]); + const buildResult = spawn("anchor", [ + "build", + "--skip-lint", + "--ignore-keys", + ]); const output = buildResult.output.toString(); const matches = output.matchAll( /global[\d]+([\w\d]+?)17.*by\s(\d+)\sbytes/g diff --git a/tests/cpi-returns/tests/cpi-return.ts b/tests/cpi-returns/tests/cpi-return.ts index 75c608e599..68353a4555 100644 --- a/tests/cpi-returns/tests/cpi-return.ts +++ b/tests/cpi-returns/tests/cpi-return.ts @@ -57,6 +57,7 @@ describe("CPI return", () => { .rpc(confirmOptions); let t = await provider.connection.getTransaction(tx, { commitment: "confirmed", + maxSupportedTransactionVersion: 0, }); const [key, data, buffer] = getReturnLog(t); @@ -81,6 +82,7 @@ describe("CPI return", () => { .rpc(confirmOptions); let t = await provider.connection.getTransaction(tx, { commitment: "confirmed", + maxSupportedTransactionVersion: 0, }); const [key, , buffer] = getReturnLog(t); assert.equal(key, calleeProgram.programId); @@ -98,6 +100,7 @@ describe("CPI return", () => { .rpc(confirmOptions); let t = await provider.connection.getTransaction(tx, { commitment: "confirmed", + maxSupportedTransactionVersion: 0, }); const [key, data, buffer] = getReturnLog(t); @@ -136,6 +139,7 @@ describe("CPI return", () => { .rpc(confirmOptions); let t = await provider.connection.getTransaction(tx, { commitment: "confirmed", + maxSupportedTransactionVersion: 0, }); const [key, data, buffer] = getReturnLog(t); diff --git a/tests/custom-discriminator/tests/custom-discriminator.ts b/tests/custom-discriminator/tests/custom-discriminator.ts index e402d46de7..e7703e696a 100644 --- a/tests/custom-discriminator/tests/custom-discriminator.ts +++ b/tests/custom-discriminator/tests/custom-discriminator.ts @@ -48,7 +48,7 @@ describe("custom-discriminator", () => { "confirmed" ); const myAccount = await program.account.myAccount.fetch( - pubkeys.myAccount + pubkeys.myAccount as anchor.web3.PublicKey ); assert.strictEqual(myAccount.field, field); }); diff --git a/tests/events/tests/events.ts b/tests/events/tests/events.ts index b90689052f..33813bcba1 100644 --- a/tests/events/tests/events.ts +++ b/tests/events/tests/events.ts @@ -68,7 +68,10 @@ describe("Events", () => { ); const txResult = await program.provider.connection.getTransaction( txHash, - { ...confirmOptions, maxSupportedTransactionVersion: 0 } + { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + } ); const ixData = anchor.utils.bytes.bs58.decode( diff --git a/tests/safety-checks/Anchor.toml b/tests/safety-checks/Anchor.toml index 51c30c2eb5..dee79f0791 100644 --- a/tests/safety-checks/Anchor.toml +++ b/tests/safety-checks/Anchor.toml @@ -1,6 +1,15 @@ +[toolchain] + +[features] +resolution = true +skip-lint = false + [programs.localnet] -unchecked_account = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" -account_info = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" +account_info = "99YPYSHjNekPHmMB7yrTxZCV2k2pRMKEqwwcqE62NqdA" +unchecked_account = "4es8ojkX4URUyar2pbuLXQ4WoFtUizb5EupfaEgHjYQF" + +[registry] +url = "https://api.apr.dev" [provider] cluster = "localnet" diff --git a/tests/safety-checks/programs/unchecked-account/Cargo.toml b/tests/safety-checks/programs/unchecked-account/Cargo.toml index c5f455db83..ecf3d17210 100644 --- a/tests/safety-checks/programs/unchecked-account/Cargo.toml +++ b/tests/safety-checks/programs/unchecked-account/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [lib] crate-type = ["cdylib", "lib"] -name = "safety_checks" +name = "unchecked_account" [features] no-entrypoint = [] diff --git a/tests/safety-checks/test.sh b/tests/safety-checks/test.sh index 872d6c1f25..d5f9071ddf 100755 --- a/tests/safety-checks/test.sh +++ b/tests/safety-checks/test.sh @@ -6,7 +6,7 @@ echo "Building programs" # Build the UncheckedAccount variant. # pushd programs/unchecked-account/ -output=$(anchor build 2>&1 > /dev/null) +output=$(anchor build --ignore-keys 2>&1 > /dev/null) if ! [[ $output =~ "Struct field \"unchecked\" is unsafe" ]]; then echo "Error: expected /// CHECK error" exit 1 @@ -17,7 +17,7 @@ popd # Build the AccountInfo variant. # pushd programs/account-info/ -output=$(anchor build 2>&1 > /dev/null) +output=$(anchor build --ignore-keys 2>&1 > /dev/null) if ! [[ $output =~ "Struct field \"unchecked\" is unsafe" ]]; then echo "Error: expected /// CHECK error" exit 1 @@ -28,7 +28,7 @@ popd # Build the control variant. # pushd programs/ignore-non-accounts/ -if ! anchor build ; then +if ! anchor build --ignore-keys ; then echo "Error: anchor build failed when it shouldn't have" exit 1 fi