Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/reusable-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ jobs:
path: tests/idl
- cmd: cd tests/lazy-account && anchor test
path: tests/lazy-account
- cmd: cd tests/signature-verification && anchor test
path: tests/signature-verification
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup/
Expand Down
20 changes: 20 additions & 0 deletions lang/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ pub enum ErrorCode {
#[msg("A transfer hook extension transfer hook program id constraint was violated")]
ConstraintMintTransferHookExtensionProgramId,

// Signature verification errors
/// 2040 - Invalid Ed25519 program id for signature verification
#[msg("Invalid Ed25519 program id for signature verification")]
Ed25519InvalidProgram,
/// 2041 - Invalid Secp256k1 program id for signature verification
#[msg("Invalid Secp256k1 program id for signature verification")]
Secp256k1InvalidProgram,
/// 2042 - Instruction unexpectedly had account metas
#[msg("Instruction unexpectedly had account metas")]
InstructionHasAccounts,
/// 2043 - Message length exceeds allowed maximum
#[msg("Message length exceeds allowed maximum")]
MessageTooLong,
/// 2045 - Invalid Secp256k1 recovery id (must be 0 or 1)
#[msg("Invalid Secp256k1 recovery id")]
InvalidRecoveryId,
/// 2047 - Signature verification failed
#[msg("Signature verification failed")]
SignatureVerificationFailed,

// Require
/// 2500 - A require expression was violated
#[msg("A require expression was violated")]
Expand Down
1 change: 1 addition & 0 deletions lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub mod error;
pub mod event;
#[doc(hidden)]
pub mod idl;
pub mod signature_verification;
pub mod system_program;
mod vec;

Expand Down
63 changes: 63 additions & 0 deletions lang/src/signature_verification/ed25519.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::error::ErrorCode;
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use solana_sdk_ids::ed25519_program;

/// Verifies an Ed25519 signature instruction assuming the signature, public key,
/// and message bytes are embedded directly inside the instruction data (Solana's
/// default encoding). Prefer [`verify_ed25519_ix_with_instruction_index`] when
/// working with custom instructions that point at external instruction data.
pub fn verify_ed25519_ix(
ix: &Instruction,
pubkey: &[u8; 32],
msg: &[u8],
sig: &[u8; 64],
) -> Result<()> {
verify_ed25519_ix_with_instruction_index(ix, u16::MAX, pubkey, msg, sig)
}

pub fn verify_ed25519_ix_with_instruction_index(
ix: &Instruction,
instruction_index: u16,
pubkey: &[u8; 32],
msg: &[u8],
sig: &[u8; 64],
) -> Result<()> {
require_keys_eq!(
ix.program_id,
ed25519_program::id(),
ErrorCode::Ed25519InvalidProgram
);
require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts);
require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong);

const DATA_START: usize = 16; // 2 header + 14 offset bytes
let pubkey_len = pubkey.len() as u16;
let sig_len = sig.len() as u16;
let msg_len = msg.len() as u16;

let sig_offset: u16 = DATA_START as u16;
let pubkey_offset: u16 = sig_offset + sig_len;
let msg_offset: u16 = pubkey_offset + pubkey_len;

let mut expected = Vec::with_capacity(DATA_START + sig.len() + pubkey.len() + msg.len());

expected.push(1u8); // num signatures
expected.push(0u8); // padding
expected.extend_from_slice(&sig_offset.to_le_bytes());
expected.extend_from_slice(&instruction_index.to_le_bytes());
expected.extend_from_slice(&pubkey_offset.to_le_bytes());
expected.extend_from_slice(&instruction_index.to_le_bytes());
expected.extend_from_slice(&msg_offset.to_le_bytes());
expected.extend_from_slice(&msg_len.to_le_bytes());
expected.extend_from_slice(&instruction_index.to_le_bytes());

expected.extend_from_slice(sig);
expected.extend_from_slice(pubkey);
expected.extend_from_slice(msg);

if expected != ix.data {
return Err(ErrorCode::SignatureVerificationFailed.into());
}
Ok(())
}
50 changes: 50 additions & 0 deletions lang/src/signature_verification/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use crate::solana_program::sysvar::instructions::{
load_current_index_checked, load_instruction_at_checked,
};
use core::convert::TryFrom;

mod ed25519;
mod secp256k1;

pub use ed25519::{verify_ed25519_ix, verify_ed25519_ix_with_instruction_index};
pub use secp256k1::{verify_secp256k1_ix, verify_secp256k1_ix_with_instruction_index};

/// Load an instruction from the Instructions sysvar at the given index.
pub fn load_instruction(index: usize, ix_sysvar: &AccountInfo<'_>) -> Result<Instruction> {
let ix = load_instruction_at_checked(index, ix_sysvar)
.map_err(|_| error!(error::ErrorCode::ConstraintRaw))?;
Ok(ix)
}

/// Loads the instruction currently executing in this transaction and verifies it
/// as an Ed25519 signature instruction.
pub fn verify_current_ed25519_instruction(
ix_sysvar: &AccountInfo<'_>,
pubkey: &[u8; 32],
msg: &[u8],
sig: &[u8; 64],
) -> Result<()> {
let idx = load_current_index_checked(ix_sysvar)
.map_err(|_| error!(error::ErrorCode::ConstraintRaw))?;
let ix = load_instruction(idx as usize, ix_sysvar)?;
verify_ed25519_ix_with_instruction_index(&ix, idx, pubkey, msg, sig)
}

/// Loads the instruction currently executing in this transaction and verifies it
/// as a Secp256k1 signature instruction.
pub fn verify_current_secp256k1_instruction(
ix_sysvar: &AccountInfo<'_>,
eth_address: &[u8; 20],
msg: &[u8],
sig: &[u8; 64],
recovery_id: u8,
) -> Result<()> {
let idx_u16 = load_current_index_checked(ix_sysvar)
.map_err(|_| error!(error::ErrorCode::ConstraintRaw))?;
let idx_u8 =
u8::try_from(idx_u16).map_err(|_| error!(error::ErrorCode::InvalidNumericConversion))?;
let ix = load_instruction(idx_u16 as usize, ix_sysvar)?;
verify_secp256k1_ix_with_instruction_index(&ix, idx_u8, eth_address, msg, sig, recovery_id)
}
68 changes: 68 additions & 0 deletions lang/src/signature_verification/secp256k1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::error::ErrorCode;
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use solana_sdk_ids::secp256k1_program;

/// Verifies a Secp256k1 instruction created under the assumption that the
/// signature, address, and message bytes all live inside the same instruction
/// (i.e. the signature ix is placed at index `0`). Prefer
/// [`verify_secp256k1_ix_with_instruction_index`] and pass the actual signature
/// instruction index instead of relying on this default.
pub fn verify_secp256k1_ix(
ix: &Instruction,
eth_address: &[u8; 20],
msg: &[u8],
sig: &[u8; 64],
recovery_id: u8,
) -> Result<()> {
verify_secp256k1_ix_with_instruction_index(ix, 0, eth_address, msg, sig, recovery_id)
}

pub fn verify_secp256k1_ix_with_instruction_index(
ix: &Instruction,
instruction_index: u8,
eth_address: &[u8; 20],
msg: &[u8],
sig: &[u8; 64],
recovery_id: u8,
) -> Result<()> {
require_keys_eq!(
ix.program_id,
secp256k1_program::id(),
ErrorCode::Secp256k1InvalidProgram
);
require_eq!(ix.accounts.len(), 0usize, ErrorCode::InstructionHasAccounts);
require!(recovery_id <= 1, ErrorCode::InvalidRecoveryId);
require!(msg.len() <= u16::MAX as usize, ErrorCode::MessageTooLong);

const DATA_START: usize = 12; // 1 header + 11 offset bytes
let eth_len = eth_address.len() as u16;
let sig_len = sig.len() as u16;
let msg_len = msg.len() as u16;

let eth_offset: u16 = DATA_START as u16;
let sig_offset: u16 = eth_offset + eth_len;
let msg_offset: u16 = sig_offset + sig_len + 1; // +1 for recovery id

let mut expected =
Vec::with_capacity(DATA_START + eth_address.len() + sig.len() + 1 + msg.len());

expected.push(1u8); // num signatures
expected.extend_from_slice(&sig_offset.to_le_bytes());
expected.push(instruction_index); // sig ix idx
expected.extend_from_slice(&eth_offset.to_le_bytes());
expected.push(instruction_index); // eth ix idx
expected.extend_from_slice(&msg_offset.to_le_bytes());
expected.extend_from_slice(&msg_len.to_le_bytes());
expected.push(instruction_index); // msg ix idx

expected.extend_from_slice(eth_address);
expected.extend_from_slice(sig);
expected.push(recovery_id);
expected.extend_from_slice(msg);

if expected != ix.data {
return Err(ErrorCode::SignatureVerificationFailed.into());
}
Ok(())
}
11 changes: 8 additions & 3 deletions lang/syn/src/idl/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,15 @@ fn get_address(acc: &Field) -> TokenStream {
fn get_pda(acc: &Field, accounts: &AccountsStruct) -> TokenStream {
let idl = get_idl_module_path();
let parse_default = |expr: &syn::Expr| parse_seed(expr, accounts);

// Seeds
let seed_constraints = acc.constraints.seeds.as_ref();
let pda = seed_constraints
let seeds_constraints = acc.constraints.seeds.as_ref().or_else(|| {
acc.constraints
.init
.as_ref()
.and_then(|init| init.seeds.as_ref())
});

let pda = seeds_constraints
.map(|seed| seed.seeds.iter().map(parse_default))
.and_then(|seeds| seeds.collect::<Result<Vec<_>>>().ok())
.and_then(|seeds| {
Expand Down
3 changes: 2 additions & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"cpi-returns",
"multiple-suites",
"multiple-suites-run-single",
"bpf-upgradeable-state"
"bpf-upgradeable-state",
"signature-verification"
],
"dependencies": {
"@project-serum/common": "^0.0.1-beta.3",
Expand Down
14 changes: 14 additions & 0 deletions tests/signature-verification/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[features]
seeds = false
resolution = true
skip-lint = false

[programs.localnet]
signature_verification_test = "9P8zSbNRQkwDrjCmqsHHcU1GTk5npaKYgKHroAkupbLG"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.ts"
7 changes: 7 additions & 0 deletions tests/signature-verification/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

[workspace]
members = ["programs/signature-verification-test"]
resolver = "2"

[profile.release]
overflow-checks = true
22 changes: 22 additions & 0 deletions tests/signature-verification/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "signature-verification",
"version": "0.31.1",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=17"
},
"scripts": {
"test": "anchor test"
},
"dependencies": {
"ethers": "^5.7.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "signature-verification-test"
version = "0.1.0"
description = "A test program for signature verification"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "signature_verification_test"

[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang = { path = "../../../../lang" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use anchor_lang::prelude::*;
use anchor_lang::signature_verification::{
load_instruction, verify_ed25519_ix_with_instruction_index,
verify_secp256k1_ix_with_instruction_index,
};

declare_id!("9P8zSbNRQkwDrjCmqsHHcU1GTk5npaKYgKHroAkupbLG");

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

pub fn verify_ed25519_signature(
ctx: Context<VerifyEd25519Signature>,
message: Vec<u8>,
signature: [u8; 64],
) -> Result<()> {
let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?;
verify_ed25519_ix_with_instruction_index(
&ix,
u16::MAX,
&ctx.accounts.signer.key().to_bytes(),
&message,
&signature,
)?;

msg!("Ed25519 signature verified successfully using custom helper!");
Ok(())
}

pub fn verify_secp(
ctx: Context<VerifySecp256k1Signature>,
message: Vec<u8>,
signature: [u8; 64],
recovery_id: u8,
eth_address: [u8; 20],
) -> Result<()> {
let ix = load_instruction(0, &ctx.accounts.ix_sysvar)?;
verify_secp256k1_ix_with_instruction_index(
&ix,
0,
&eth_address,
&message,
&signature,
recovery_id,
)?;

msg!("Secp256k1 signature verified successfully using custom helper!");

Ok(())
}
}

#[derive(Accounts)]
pub struct VerifyEd25519Signature<'info> {
/// CHECK: Signer account
pub signer: AccountInfo<'info>,
/// CHECK: Instructions sysvar account
#[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
pub ix_sysvar: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct VerifySecp256k1Signature<'info> {
/// CHECK: Instructions sysvar account
#[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
pub ix_sysvar: AccountInfo<'info>,
}
Loading
Loading