Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -462,6 +462,8 @@ jobs:
path: tests/idl
- cmd: cd tests/lazy-account && anchor test
path: tests/lazy-account
- cmd: cd tests/signature-verification && anchor test --skip-lint
Comment thread
Otter-0x4ka5h marked this conversation as resolved.
Outdated
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
Comment thread
Otter-0x4ka5h marked this conversation as resolved.
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.push(0u8); // sig ix idx
Comment thread
Otter-0x4ka5h marked this conversation as resolved.
Outdated
expected.extend_from_slice(&pubkey_offset.to_le_bytes());
expected.push(0u8); // pubkey 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(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
Member 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(())
}
57 changes: 57 additions & 0 deletions lang/src/signature_verifications/ed25519.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::error::ErrorCode;
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use solana_sdk_ids::ed25519_program;

/// Verify that `ix.data` is an Ed25519 syscall verifying `sig` over `msg` by `pubkey`.
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);

let num_signatures: u8 = 1;
let padding: u8 = 0;
let header_len: usize = 2;
let offsets_len: usize = 14; // 7 * u16
let base: usize = header_len + offsets_len;

let signature_offset: u16 = base as u16;
let signature_instruction_index: u16 = u16::MAX;
let public_key_offset: u16 = (base + 64) as u16;
let public_key_instruction_index: u16 = u16::MAX;
let message_data_offset: u16 = (base + 64 + 32) as u16; // 32 bytes for pubkey
let message_data_size: u16 = msg.len() as u16;
let message_instruction_index: u16 = u16::MAX;

// [header][offsets][signature][public_key][msg]
let mut expected = Vec::with_capacity(base + 64 + 32 + msg.len());
expected.push(num_signatures);
expected.push(padding);
expected.extend_from_slice(&signature_offset.to_le_bytes());
expected.extend_from_slice(&signature_instruction_index.to_le_bytes());
expected.extend_from_slice(&public_key_offset.to_le_bytes());
expected.extend_from_slice(&public_key_instruction_index.to_le_bytes());
expected.extend_from_slice(&message_data_offset.to_le_bytes());
expected.extend_from_slice(&message_data_size.to_le_bytes());
expected.extend_from_slice(&message_instruction_index.to_le_bytes());
expected.extend_from_slice(sig);
expected.extend_from_slice(pubkey);
expected.extend_from_slice(msg);

if ix.data.len() != expected.len() {
return Err(error!(ErrorCode::DataLengthMismatch));
}
if ix.data != expected {
return Err(error!(ErrorCode::ConstraintRaw));
}
Ok(())
}
16 changes: 16 additions & 0 deletions lang/src/signature_verifications/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)
}
64 changes: 64 additions & 0 deletions lang/src/signature_verifications/secp256k1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::error::ErrorCode;
use crate::prelude::*;
use crate::solana_program::instruction::Instruction;
use solana_sdk_ids::secp256k1_program;

/// Verify that `ix.data` is a Secp256k1 syscall verifying `sig` over `msg` by `eth_address` with `recovery_id`.
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);

let num_signatures: u8 = 1;
let num_eth_addresses: u8 = 1;
let header_len: usize = 2;
let offsets_len: usize = 18; // 9 * u16
let base: usize = header_len + offsets_len;

let signature_offset: u16 = base as u16;
let signature_instruction_index: u16 = u16::MAX;
let eth_address_offset: u16 = (base + 64) as u16;
let eth_address_instruction_index: u16 = u16::MAX;
let message_data_offset: u16 = (base + 64 + 20) as u16; // 20 bytes for eth_address
let message_data_size: u16 = msg.len() as u16;
let message_instruction_index: u16 = u16::MAX;
let recovery_id_offset: u16 = (base + 64 + 20 + msg.len()) as u16;
let recovery_id_instruction_index: u16 = u16::MAX;

// [header][offsets][signature][eth_address][msg][recovery_id]
let mut expected = Vec::with_capacity(base + 64 + 20 + msg.len() + 1);
expected.push(num_signatures);
expected.push(num_eth_addresses);
expected.extend_from_slice(&signature_offset.to_le_bytes());
expected.extend_from_slice(&signature_instruction_index.to_le_bytes());
expected.extend_from_slice(&eth_address_offset.to_le_bytes());
expected.extend_from_slice(&eth_address_instruction_index.to_le_bytes());
expected.extend_from_slice(&message_data_offset.to_le_bytes());
expected.extend_from_slice(&message_data_size.to_le_bytes());
expected.extend_from_slice(&message_instruction_index.to_le_bytes());
expected.extend_from_slice(&recovery_id_offset.to_le_bytes());
expected.extend_from_slice(&recovery_id_instruction_index.to_le_bytes());
expected.extend_from_slice(sig);
expected.extend_from_slice(eth_address);
expected.extend_from_slice(msg);
expected.push(recovery_id);

if ix.data.len() != expected.len() {
return Err(error!(ErrorCode::DataLengthMismatch));
}
if ix.data != expected {
return Err(error!(ErrorCode::ConstraintRaw));
}
Ok(())
}
3 changes: 2 additions & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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-verifications",
"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 = []
Loading
Loading