Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
49 changes: 49 additions & 0 deletions lang/src/signature_verification/ed25519.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::error::ErrorCode;
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use solana_sdk_ids::ed25519_program;

pub fn verify_ed25519_ix(
ix: &Instruction,
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(&(u16::MAX).to_le_bytes());
expected.extend_from_slice(&pubkey_offset.to_le_bytes());
expected.extend_from_slice(&(u16::MAX).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(&(u16::MAX).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(())
}
16 changes: 16 additions & 0 deletions lang/src/signature_verification/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use crate::solana_program::sysvar::instructions::load_instruction_at_checked;

mod ed25519;
mod secp256k1;

pub use ed25519::verify_ed25519_ix;
pub use secp256k1::verify_secp256k1_ix;

/// 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)
}
52 changes: 52 additions & 0 deletions lang/src/signature_verification/secp256k1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::error::ErrorCode;
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use solana_sdk_ids::secp256k1_program;

pub fn verify_secp256k1_ix(
ix: &Instruction,
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(0u8); // sig ix idx
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we change this to always index into the current instruction (would require us to call solana_program::sysvar::instructions::load_current_index_checked) or add the index as an argument?

I think it's a bit confusing if verify_ed25519_ix operates on the current instruction, but verify_secp256k1_ix defaults to the first one.

Maybe both this functions should take an index as argument and we add two helper functions which always call them with the current index?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added

expected.extend_from_slice(&eth_offset.to_le_bytes());
expected.push(0u8); // eth ix idx
expected.extend_from_slice(&msg_offset.to_le_bytes());
expected.extend_from_slice(&msg_len.to_le_bytes());
expected.push(0u8); // 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(())
}
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,59 @@
use anchor_lang::prelude::*;
use anchor_lang::signature_verification::{
load_instruction, verify_ed25519_ix, verify_secp256k1_ix,
};

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(
&ix,
&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(&ix, &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