Expand description

Instructions for the secp256k1 native program.

This module provides low-level cryptographic building blocks that must be used carefully to ensure proper security. Read this documentation and accompanying links thoroughly.

The secp26k1 native program peforms flexible verification of secp256k1 ECDSA signatures, as used by Ethereum. It can verify up to 255 signatures on up to 255 messages, with those signatures, messages, and their public keys arbitrarily distributed across the instruction data of any instructions in the same transaction as the secp256k1 instruction.

The secp256k1 native program ID is located in the secp256k1_program module.

The instruction is designed for Ethereum interoperability, but may be useful for other purposes. It operates on Ethereum addresses, which are keccak hashes of secp256k1 public keys, and internally is implemented using the secp256k1 key recovery algorithm. Ethereum address can be created for secp256k1 public keys with the construct_eth_pubkey function.

This instruction does not directly allow for key recovery as in Ethereum’s ecrecover precompile. For that Safecoin provides the secp256k1_recover syscall.

Use cases for the secp256k1 instruction include:

  • Verifying Ethereum transaction signatures.
  • Verifying Ethereum EIP-712 signatures.
  • Verifying arbitrary secp256k1 signatures.
  • Signing a single message with multiple signatures.

The new_secp256k1_instruction function is suitable for building a secp256k1 program instruction for basic use cases were a single message must be signed by a known secret key. For other uses cases, including many Ethereum-integration use cases, construction of the secp256k1 instruction must be done manually.

How to use this program

Transactions that uses the secp256k1 native program will typically include at least two instructions: one for the secp256k1 program to verify the signatures, and one for a custom program that will check that the secp256k1 instruction data matches what the program expects (using load_instruction_at_checked or get_instruction_relative). The signatures, messages, and Ethereum addresses being verified may reside in the instruction data of either of these instructions, or in the instruction data of one or more additional instructions, as long as those instructions are in the same transaction.

Correct use of this program involves multiple steps, in client code and program code:

  • In the client:
    • Sign the keccak-hashed messages with a secp256k1 ECDSA library, like the libsecp256k1 crate.
    • Build any custom instruction data that contain signature, message, or Ethereum address data that will be used by the secp256k1 instruction.
    • Build the secp256k1 program instruction data, specifying the number of signatures to verify, the instruction indexes within the transaction, and offsets within those instruction’s data, where the signatures, messages, and Ethereum addresses are located.
    • Build the custom instruction for the program that will check the results of the secp256k1 native program.
    • Package all instructions into a single transaction and submit them.
  • In the program:

The signature, message, or Ethereum addresses may reside in the secp256k1 instruction data itself as additional data, their bytes following the bytes of the protocol required by the secp256k1 instruction to locate the signature, message, and Ethereum address data. This is the technique used by new_secp256k1_instruction for simple signature verification.

The solana_sdk crate provides few APIs for building the instructions and transactions necessary for properly using the secp256k1 native program. Many steps must be done manually.

The solana_program crate provides no APIs to assist in interpreting the the secp256k1 instruction data. It must be done manually.

The secp256k1 program is implemented with the libsecp256k1 crate, which clients may also want to use.

Layout and interpretation of the secp256k1 instruction data

The secp256k1 instruction data contains:

  • 1 byte indicating the number of signatures to verify, 0 - 255,
  • A number of signature offset structures that indicate where in the transaction to locate each signature, message, and Ethereum address.
  • 0 or more bytes of arbitrary data, which may contain signatures, messages or Ethereum addresses.

The signature offset structure is defined by SecpSignatureOffsets, and can be serialized to the correct format with bincode::serialize_into. Note that the bincode format may not be stable, and callers should ensure they use the same version of bincode as the Safecoin SDK. This data structure is not provided to Safecoin programs, which are expected to interpret the signature offsets manually.

The serialized signature offset structure has the following 11-byte layout, with data types in little-endian encoding.

indexbytestypedescription
02u16signature_offset - offset to 64-byte signature plus 1-byte recovery ID.
21u8signature_offset_instruction_index - within the transaction, the index of the transaction whose instruction data contains the signature.
32u16eth_address_offset - offset to 20-byte Ethereum address.
51u8eth_address_instruction_index - within the transaction, the index of the instruction whose instruction data contains the Ethereum address.
62u16message_data_offset - Offset to start of message data.
82u16message_data_size - Size of message data in bytes.
101u8message_instruction_index - Within the transaction, the index of the instruction whose instruction data contains the message data.

Signature malleability

With the ECDSA signature algorithm it is possible for any party, given a valid signature of some message, to create a second signature that is equally valid. This is known as signature malleability. In many cases this is not a concern, but in cases where applications rely on signatures to have a unique representation this can be the source of bugs, potentially with security implications.

The safecoin secp256k1_recover function does not prevent signature malleability. This is in contrast to the Bitcoin secp256k1 library, which does prevent malleability by default. Safecoin accepts signatures with S values that are either in the high order or in the low order, and it is trivial to produce one from the other.

For more complete documentation of the subject, and techniques to prevent malleability, see the documentation for the secp256k1_recover syscall.

Additional security considerations

Most programs will want to be conservative about the layout of the secp256k1 instruction to prevent unforeseen bugs. The following checks may be desirable:

  • That there are exactly the expected number of signatures.
  • That the three indexes, signature_offset_instruction_index, eth_address_instruction_index, and message_instruction_index are as expected, placing the signature, message and Ethereum address in the expected instruction.

Loading the secp256k1 instruction data within a program requires access to the instructions sysvar, which must be passed to the program by its caller. Programs must verify the ID of this program to avoid calling an imposter program. This does not need to be done manually though, as long as it is only used through the load_instruction_at_checked or get_instruction_relative functions. Both of these functions check their sysvar argument to ensure it is the known instruction sysvar.

Programs should always verify that the secp256k1 program ID loaded through the instructions sysvar has the same value as in the secp256k1_program module. Again this prevents imposter programs.

Errors

The transaction will fail if any of the following are true:

  • Any signature was not created by the secret key corresponding to the specified public key.
  • Any signature is invalid.
  • Any signature is “overflowing”, a non-standard condition.
  • The instruction data is empty.
  • The first byte of instruction data is equal to 0 (indicating no signatures), but the instruction data’s length is greater than 1.
  • The instruction data is not long enough to hold the number of signature offsets specified in the first byte.
  • Any instruction indexes specified in the signature offsets are greater or equal to the number of instructions in the transaction.
  • Any bounds specified in the signature offsets exceed the bounds of the instruction data to which they are indexed.

Examples

Both of the following examples make use of the following module definition to parse the secp256k1 instruction data from within a Safecoin program.

mod secp256k1_defs {
    use solana_program::program_error::ProgramError;
    use std::iter::Iterator;

    pub const HASHED_PUBKEY_SERIALIZED_SIZE: usize = 20;
    pub const SIGNATURE_SERIALIZED_SIZE: usize = 64;
    pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 11;

    /// The structure encoded in the secp2256k1 instruction data.
    pub struct SecpSignatureOffsets {
        pub signature_offset: u16,
        pub signature_instruction_index: u8,
        pub eth_address_offset: u16,
        pub eth_address_instruction_index: u8,
        pub message_data_offset: u16,
        pub message_data_size: u16,
        pub message_instruction_index: u8,
    }

    pub fn iter_signature_offsets(
       secp256k1_instr_data: &[u8],
    ) -> Result<impl Iterator<Item = SecpSignatureOffsets> + '_, ProgramError> {
        // First element is the number of `SecpSignatureOffsets`.
        let num_structs = *secp256k1_instr_data
            .get(0)
            .ok_or(ProgramError::InvalidArgument)?;

        let all_structs_size = SIGNATURE_OFFSETS_SERIALIZED_SIZE * num_structs as usize;
        let all_structs_slice = secp256k1_instr_data
            .get(1..all_structs_size + 1)
            .ok_or(ProgramError::InvalidArgument)?;

        fn decode_u16(chunk: &[u8], index: usize) -> u16 {
            u16::from_le_bytes(<[u8; 2]>::try_from(&chunk[index..index + 2]).unwrap())
        }

        Ok(all_structs_slice
            .chunks(SIGNATURE_OFFSETS_SERIALIZED_SIZE)
            .map(|chunk| SecpSignatureOffsets {
                signature_offset: decode_u16(chunk, 0),
                signature_instruction_index: chunk[2],
                eth_address_offset: decode_u16(chunk, 3),
                eth_address_instruction_index: chunk[5],
                message_data_offset: decode_u16(chunk, 6),
                message_data_size: decode_u16(chunk, 8),
                message_instruction_index: chunk[10],
            }))
    }
}

Example: Signing and verifying with new_secp256k1_instruction

This example demonstrates the simplest way to use the secp256k1 program, by calling new_secp256k1_instruction to sign a single message and build the corresponding secp256k1 instruction.

This example has two components: a Safecoin program, and an RPC client that sends a transaction to call it. The RPC client will sign a single message, and the Safecoin program will introspect the secp256k1 instruction to verify that the signer matches a known authorized public key.

The Safecoin program. Note that it uses libsecp256k1 version 0.7.0 to parse the secp256k1 signature to prevent malleability.

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    secp256k1_program,
    sysvar,
};

/// An Ethereum address corresponding to a secp256k1 secret key that is
/// authorized to sign our messages.
const AUTHORIZED_ETH_ADDRESS: [u8; 20] = [
    0x18, 0x8a, 0x5c, 0xf2, 0x3b, 0x0e, 0xff, 0xe9, 0xa8, 0xe1, 0x42, 0x64, 0x5b, 0x82, 0x2f, 0x3a,
    0x6b, 0x8b, 0x52, 0x35,
];

/// Check the secp256k1 instruction to ensure it was signed by
/// `AUTHORIZED_ETH_ADDRESS`s key.
///
/// `accounts` is the slice of all accounts passed to the program
/// entrypoint. The only account it should contain is the instructions sysvar.
fn demo_secp256k1_verify_basic(
   accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    // The instructions sysvar gives access to the instructions in the transaction.
    let instructions_sysvar_account = next_account_info(account_info_iter)?;
    assert!(sysvar::instructions::check_id(
        instructions_sysvar_account.key
    ));

    // Load the secp256k1 instruction.
    // `new_secp256k1_instruction` generates an instruction that must be at index 0.
    let secp256k1_instr =
        sysvar::instructions::load_instruction_at_checked(0, instructions_sysvar_account)?;

    // Verify it is a secp256k1 instruction.
    // This is security-critical - what if the transaction uses an imposter secp256k1 program?
    assert!(secp256k1_program::check_id(&secp256k1_instr.program_id));

    // There must be at least one byte. This is also verified by the runtime,
    // and doesn't strictly need to be checked.
    assert!(secp256k1_instr.data.len() > 1);

    let num_signatures = secp256k1_instr.data[0];
    // `new_secp256k1_instruction` generates an instruction that contains one signature.
    assert_eq!(1, num_signatures);

    // Load the first and only set of signature offsets.
    let offsets: secp256k1_defs::SecpSignatureOffsets =
        secp256k1_defs::iter_signature_offsets(&secp256k1_instr.data)?
            .next()
            .ok_or(ProgramError::InvalidArgument)?;

    // `new_secp256k1_instruction` generates an instruction that only uses instruction index 0.
    assert_eq!(0, offsets.signature_instruction_index);
    assert_eq!(0, offsets.eth_address_instruction_index);
    assert_eq!(0, offsets.message_instruction_index);

    // Reject high-s value signatures to prevent malleability.
    // Safecoin does not do this itself.
    // This may or may not be necessary depending on use case.
    {
        let signature = &secp256k1_instr.data[offsets.signature_offset as usize
            ..offsets.signature_offset as usize + secp256k1_defs::SIGNATURE_SERIALIZED_SIZE];
        let signature = libsecp256k1::Signature::parse_standard_slice(signature)
            .map_err(|_| ProgramError::InvalidArgument)?;

        if signature.s.is_high() {
            msg!("signature with high-s value");
            return Err(ProgramError::InvalidArgument);
        }
    }

    // There is likely at least one more verification step a real program needs
    // to do here to ensure it trusts the secp256k1 instruction, e.g.:
    //
    // - verify the tx signer is authorized
    // - verify the secp256k1 signer is authorized

    // Here we are checking the secp256k1 pubkey against a known authorized pubkey.
    let eth_address = &secp256k1_instr.data[offsets.eth_address_offset as usize
        ..offsets.eth_address_offset as usize + secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE];

    if eth_address != AUTHORIZED_ETH_ADDRESS {
        return Err(ProgramError::InvalidArgument);
    }

    Ok(())
}

The client program:

use anyhow::Result;
use safecoin_client::rpc_client::RpcClient;
use solana_sdk::{
    instruction::{AccountMeta, Instruction},
    secp256k1_instruction,
    signature::{Keypair, Signer},
    sysvar,
    transaction::Transaction,
};

fn demo_secp256k1_verify_basic(
    payer_keypair: &Keypair,
    secp256k1_secret_key: &libsecp256k1::SecretKey,
    client: &RpcClient,
    program_keypair: &Keypair,
) -> Result<()> {
    // Internally to `new_secp256k1_instruction` and
    // `secp256k_instruction::verify` (the secp256k1 program), this message is
    // keccak-hashed before signing.
    let msg = b"hello world";
    let secp256k1_instr = secp256k1_instruction::new_secp256k1_instruction(&secp256k1_secret_key, msg);

    let program_instr = Instruction::new_with_bytes(
        program_keypair.pubkey(),
        &[],
        vec![
            AccountMeta::new_readonly(sysvar::instructions::ID, false)
        ],
    );

    let blockhash = client.get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &[secp256k1_instr, program_instr],
        Some(&payer_keypair.pubkey()),
        &[payer_keypair],
        blockhash,
    );

    client.send_and_confirm_transaction(&tx)?;

    Ok(())
}

Example: Verifying multiple signatures in one instruction

This examples demonstrates manually creating a secp256k1 instruction containing many signatures, and a Safecoin program that parses them all. This example on its own has no practical purpose. It simply demonstrates advanced use of the secp256k1 program.

Recall that the secp256k1 program will accept signatures, messages, and Ethereum addresses that reside in any instruction contained in the same transaction. In the previous example, the Safecoin program asserted that all signatures, messages, and addresses were stored in the instruction at 0. In this next example the Safecoin program supports signatures, messages, and addresses stored in any instruction. For simplicity the client still only stores signatures, messages, and addresses in a single instruction, the secp256k1 instruction. The code for storing this data across multiple instructions would be complex, and may not be necessary in practice.

This example has two components: a Safecoin program, and an RPC client that sends a transaction to call it.

The Safecoin program:

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    secp256k1_program,
    sysvar,
};

/// A struct to hold the values specified in the `SecpSignatureOffsets` struct.
struct SecpSignature {
    signature: [u8; secp256k1_defs::SIGNATURE_SERIALIZED_SIZE],
    recovery_id: u8,
    eth_address: [u8; secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE],
    message: Vec<u8>,
}

/// Load all signatures indicated in the secp256k1 instruction.
///
/// This function is quite inefficient for reloading the same instructions
/// repeatedly and making copies and allocations.
fn load_signatures(
    secp256k1_instr_data: &[u8],
    instructions_sysvar_account: &AccountInfo,
) -> Result<Vec<SecpSignature>, ProgramError> {
    let mut sigs = vec![];
    for offsets in secp256k1_defs::iter_signature_offsets(secp256k1_instr_data)? {
        let signature_instr = sysvar::instructions::load_instruction_at_checked(
            offsets.signature_instruction_index as usize,
            instructions_sysvar_account,
        )?;
        let eth_address_instr = sysvar::instructions::load_instruction_at_checked(
            offsets.eth_address_instruction_index as usize,
            instructions_sysvar_account,
        )?;
        let message_instr = sysvar::instructions::load_instruction_at_checked(
            offsets.message_instruction_index as usize,
            instructions_sysvar_account,
        )?;

        // These indexes must all be valid because the runtime already verified them.
        let signature = &signature_instr.data[offsets.signature_offset as usize
            ..offsets.signature_offset as usize + secp256k1_defs::SIGNATURE_SERIALIZED_SIZE];
        let recovery_id = signature_instr.data
            [offsets.signature_offset as usize + secp256k1_defs::SIGNATURE_SERIALIZED_SIZE];
        let eth_address = &eth_address_instr.data[offsets.eth_address_offset as usize
            ..offsets.eth_address_offset as usize + secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE];
        let message = &message_instr.data[offsets.message_data_offset as usize
            ..offsets.message_data_offset as usize + offsets.message_data_size as usize];

        let signature =
            <[u8; secp256k1_defs::SIGNATURE_SERIALIZED_SIZE]>::try_from(signature).unwrap();
        let eth_address =
            <[u8; secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE]>::try_from(eth_address).unwrap();
        let message = Vec::from(message);

        sigs.push(SecpSignature {
            signature,
            recovery_id,
            eth_address,
            message,
        })
    }
    Ok(sigs)
}

fn demo_secp256k1_custom_many(
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    let instructions_sysvar_account = next_account_info(account_info_iter)?;
    assert!(sysvar::instructions::check_id(
        instructions_sysvar_account.key
    ));

    let secp256k1_instr =
        sysvar::instructions::get_instruction_relative(-1, instructions_sysvar_account)?;

    assert!(secp256k1_program::check_id(&secp256k1_instr.program_id));

    let signatures = load_signatures(&secp256k1_instr.data, instructions_sysvar_account)?;
    for (idx, signature_bundle) in signatures.iter().enumerate() {
        let signature = hex::encode(&signature_bundle.signature);
        let eth_address = hex::encode(&signature_bundle.eth_address);
        let message = hex::encode(&signature_bundle.message);
        msg!("sig {}: {:?}", idx, signature);
        msg!("recid: {}: {}", idx, signature_bundle.recovery_id);
        msg!("eth address {}: {}", idx, eth_address);
        msg!("message {}: {}", idx, message);
    }

    Ok(())
}

The client program:

use anyhow::Result;
use safecoin_client::rpc_client::RpcClient;
use solana_sdk::{
    instruction::{AccountMeta, Instruction},
    keccak,
    secp256k1_instruction::{
        self, SecpSignatureOffsets, HASHED_PUBKEY_SERIALIZED_SIZE,
        SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE,
    },
    signature::{Keypair, Signer},
    sysvar,
    transaction::Transaction,
};

/// A struct to hold the values specified in the `SecpSignatureOffsets` struct.
struct SecpSignature {
    signature: [u8; SIGNATURE_SERIALIZED_SIZE],
    recovery_id: u8,
    eth_address: [u8; HASHED_PUBKEY_SERIALIZED_SIZE],
    message: Vec<u8>,
}

/// Create the instruction data for a secp256k1 instruction.
///
/// `instruction_index` is the index the secp256k1 instruction will appear
/// within the transaction. For simplicity, this function only supports packing
/// the signatures into the secp256k1 instruction data, and not into any other
/// instructions within the transaction.
fn make_secp256k1_instruction_data(
    signatures: &[SecpSignature],
    instruction_index: u8,
) -> Result<Vec<u8>> {
    assert!(signatures.len() <= u8::max_value().into());

    // We're going to pack all the signatures into the secp256k1 instruction data.
    // Before our signatures though is the signature offset structures
    // the secp256k1 program parses to find those signatures.
    // This value represents the byte offset where the signatures begin.
    let data_start = 1 + signatures.len() * SIGNATURE_OFFSETS_SERIALIZED_SIZE;

    let mut signature_offsets = vec![];
    let mut signature_buffer = vec![];

    for signature_bundle in signatures {
        let data_start = data_start
            .checked_add(signature_buffer.len())
            .expect("overflow");

        let signature_offset = data_start;
        let eth_address_offset = data_start
            .checked_add(SIGNATURE_SERIALIZED_SIZE + 1)
            .expect("overflow");
        let message_data_offset = eth_address_offset
            .checked_add(HASHED_PUBKEY_SERIALIZED_SIZE)
            .expect("overflow");
        let message_data_size = signature_bundle.message.len();

        let signature_offset = u16::try_from(signature_offset)?;
        let eth_address_offset = u16::try_from(eth_address_offset)?;
        let message_data_offset = u16::try_from(message_data_offset)?;
        let message_data_size = u16::try_from(message_data_size)?;

        signature_offsets.push(SecpSignatureOffsets {
            signature_offset,
            signature_instruction_index: instruction_index,
            eth_address_offset,
            eth_address_instruction_index: instruction_index,
            message_data_offset,
            message_data_size,
            message_instruction_index: instruction_index,
        });

        signature_buffer.extend(signature_bundle.signature);
        signature_buffer.push(signature_bundle.recovery_id);
        signature_buffer.extend(&signature_bundle.eth_address);
        signature_buffer.extend(&signature_bundle.message);
    }

    let mut instr_data = vec![];
    instr_data.push(signatures.len() as u8);

    for offsets in signature_offsets {
        let offsets = bincode::serialize(&offsets)?;
        instr_data.extend(offsets);
    }

    instr_data.extend(signature_buffer);

    Ok(instr_data)
}

fn demo_secp256k1_custom_many(
    payer_keypair: &Keypair,
    client: &RpcClient,
    program_keypair: &Keypair,
) -> Result<()> {
    // Sign some messages.
    let mut signatures = vec![];
    for idx in 0..2 {
        let secret_key = libsecp256k1::SecretKey::random(&mut rand::thread_rng());
        let message = format!("hello world {}", idx).into_bytes();
        let message_hash = {
            let mut hasher = keccak::Hasher::default();
            hasher.hash(&message);
            hasher.result()
        };
        let secp_message = libsecp256k1::Message::parse(&message_hash.0);
        let (signature, recovery_id) = libsecp256k1::sign(&secp_message, &secret_key);
        let signature = signature.serialize();
        let recovery_id = recovery_id.serialize();

        let public_key = libsecp256k1::PublicKey::from_secret_key(&secret_key);
        let eth_address = secp256k1_instruction::construct_eth_pubkey(&public_key);

        signatures.push(SecpSignature {
            signature,
            recovery_id,
            eth_address,
            message,
        });
    }

    let secp256k1_instr_data = make_secp256k1_instruction_data(&signatures, 0)?;
    let secp256k1_instr = Instruction::new_with_bytes(
        solana_sdk::secp256k1_program::ID,
        &secp256k1_instr_data,
        vec![],
    );

    let program_instr = Instruction::new_with_bytes(
        program_keypair.pubkey(),
        &[],
        vec![
            AccountMeta::new_readonly(sysvar::instructions::ID, false)
        ],
    );

    let blockhash = client.get_latest_blockhash()?;
    let tx = Transaction::new_signed_with_payer(
        &[secp256k1_instr, program_instr],
        Some(&payer_keypair.pubkey()),
        &[payer_keypair],
        blockhash,
    );

    client.send_and_confirm_transaction(&tx)?;

    Ok(())
}

Structs

Constants

Functions

  • Creates an Ethereum address from a secp256k1 public key.
  • Sign a message and create a secp256k1 program instruction to verify the signature.
  • Verifies the signatures specified in the secp256k1 instruction data.