Add keccak-secp256k1 instruction (#11839)
* Implement keccak-secp256k1 instruction Verifies eth addreses with ecrecover function * Move secp256k1 test
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
use crate::clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT};
|
||||
use crate::message::Message;
|
||||
use crate::secp256k1_program;
|
||||
use log::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, AbiExample)]
|
||||
@ -18,6 +19,11 @@ impl Default for FeeCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FeeConfig {
|
||||
pub is_secp256k1_enabled: bool,
|
||||
}
|
||||
|
||||
impl FeeCalculator {
|
||||
pub fn new(lamports_per_signature: u64) -> Self {
|
||||
Self {
|
||||
@ -25,8 +31,27 @@ impl FeeCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_fee(&self, message: &Message) -> u64 {
|
||||
self.lamports_per_signature * u64::from(message.header.num_required_signatures)
|
||||
// extra_config: None == everything enabled
|
||||
pub fn calculate_fee(&self, message: &Message, extra_config: Option<FeeConfig>) -> u64 {
|
||||
let is_secp256k1_enabled = match extra_config {
|
||||
Some(config) => config.is_secp256k1_enabled,
|
||||
None => true,
|
||||
};
|
||||
let mut num_secp_signatures: u64 = 0;
|
||||
if is_secp256k1_enabled {
|
||||
for instruction in &message.instructions {
|
||||
let program_index = instruction.program_id_index as usize;
|
||||
// Transaction may not be sanitized here
|
||||
if program_index < message.account_keys.len() {
|
||||
let id = message.account_keys[program_index];
|
||||
if secp256k1_program::check_id(&id) && !instruction.data.is_empty() {
|
||||
num_secp_signatures += instruction.data[0] as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.lamports_per_signature
|
||||
* (u64::from(message.header.num_required_signatures) + num_secp_signatures)
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,25 +207,76 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_fee_calculator_calculate_fee() {
|
||||
let fee_config = Some(FeeConfig {
|
||||
is_secp256k1_enabled: true,
|
||||
});
|
||||
// Default: no fee.
|
||||
let message = Message::default();
|
||||
assert_eq!(FeeCalculator::default().calculate_fee(&message), 0);
|
||||
assert_eq!(
|
||||
FeeCalculator::default().calculate_fee(&message, fee_config.clone()),
|
||||
0
|
||||
);
|
||||
|
||||
// No signature, no fee.
|
||||
assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 0);
|
||||
assert_eq!(FeeCalculator::new(1).calculate_fee(&message, fee_config), 0);
|
||||
|
||||
let fee_config = Some(FeeConfig {
|
||||
is_secp256k1_enabled: false,
|
||||
});
|
||||
// One signature, a fee.
|
||||
let pubkey0 = Pubkey::new(&[0; 32]);
|
||||
let pubkey1 = Pubkey::new(&[1; 32]);
|
||||
let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1);
|
||||
let message = Message::new(&[ix0], Some(&pubkey0));
|
||||
assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 2);
|
||||
assert_eq!(FeeCalculator::new(2).calculate_fee(&message, fee_config), 2);
|
||||
|
||||
// Two signatures, double the fee.
|
||||
let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1);
|
||||
let ix1 = system_instruction::transfer(&pubkey1, &pubkey0, 1);
|
||||
let message = Message::new(&[ix0, ix1], Some(&pubkey0));
|
||||
assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 4);
|
||||
assert_eq!(FeeCalculator::new(2).calculate_fee(&message, None), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_calculator_calculate_fee_secp256k1() {
|
||||
use crate::instruction::Instruction;
|
||||
let pubkey0 = Pubkey::new(&[0; 32]);
|
||||
let pubkey1 = Pubkey::new(&[1; 32]);
|
||||
let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1);
|
||||
let mut secp_instruction = Instruction {
|
||||
program_id: crate::secp256k1_program::id(),
|
||||
accounts: vec![],
|
||||
data: vec![],
|
||||
};
|
||||
let mut secp_instruction2 = Instruction {
|
||||
program_id: crate::secp256k1_program::id(),
|
||||
accounts: vec![],
|
||||
data: vec![1],
|
||||
};
|
||||
|
||||
let message = Message::new(
|
||||
&[
|
||||
ix0.clone(),
|
||||
secp_instruction.clone(),
|
||||
secp_instruction2.clone(),
|
||||
],
|
||||
Some(&pubkey0),
|
||||
);
|
||||
let fee_config = Some(FeeConfig {
|
||||
is_secp256k1_enabled: true,
|
||||
});
|
||||
assert_eq!(
|
||||
FeeCalculator::new(1).calculate_fee(&message, fee_config.clone()),
|
||||
2
|
||||
);
|
||||
|
||||
secp_instruction.data = vec![0];
|
||||
secp_instruction2.data = vec![10];
|
||||
let message = Message::new(&[ix0, secp_instruction, secp_instruction2], Some(&pubkey0));
|
||||
assert_eq!(
|
||||
FeeCalculator::new(1).calculate_fee(&message, fee_config),
|
||||
11
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -37,6 +37,7 @@ pub mod pubkey;
|
||||
pub mod rent;
|
||||
pub mod rpc_port;
|
||||
pub mod sanitize;
|
||||
pub mod secp256k1_program;
|
||||
pub mod short_vec;
|
||||
pub mod slot_hashes;
|
||||
pub mod slot_history;
|
||||
@ -89,6 +90,8 @@ pub mod genesis_config;
|
||||
#[cfg(not(feature = "program"))]
|
||||
pub mod hard_forks;
|
||||
#[cfg(not(feature = "program"))]
|
||||
pub mod secp256k1;
|
||||
#[cfg(not(feature = "program"))]
|
||||
pub mod shred_version;
|
||||
#[cfg(not(feature = "program"))]
|
||||
pub mod signature;
|
||||
|
146
sdk/src/secp256k1.rs
Normal file
146
sdk/src/secp256k1.rs
Normal file
@ -0,0 +1,146 @@
|
||||
use crate::clock::{Epoch, GENESIS_EPOCH};
|
||||
use crate::fee_calculator::FeeConfig;
|
||||
use crate::genesis_config::ClusterType;
|
||||
use digest::Digest;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
pub fn get_fee_config(cluster_type: ClusterType, epoch: Epoch) -> Option<FeeConfig> {
|
||||
Some(FeeConfig {
|
||||
is_secp256k1_enabled: is_enabled(cluster_type, epoch),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_enabled_epoch(cluster_type: ClusterType) -> Epoch {
|
||||
match cluster_type {
|
||||
ClusterType::Development => GENESIS_EPOCH,
|
||||
ClusterType::Testnet => u64::MAX,
|
||||
ClusterType::MainnetBeta => u64::MAX,
|
||||
ClusterType::Devnet => u64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(cluster_type: ClusterType, epoch: Epoch) -> bool {
|
||||
epoch >= is_enabled_epoch(cluster_type)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Secp256k1Error {
|
||||
InvalidSignature,
|
||||
InvalidRecoveryId,
|
||||
InvalidDataOffsets,
|
||||
InvalidInstructionDataSize,
|
||||
}
|
||||
|
||||
pub const HASHED_PUBKEY_SERIALIZED_SIZE: usize = 20;
|
||||
pub const SIGNATURE_SERIALIZED_SIZE: usize = 64;
|
||||
pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 11;
|
||||
|
||||
pub fn construct_eth_pubkey(pubkey: &secp256k1::PublicKey) -> [u8; HASHED_PUBKEY_SERIALIZED_SIZE] {
|
||||
let mut addr = [0u8; HASHED_PUBKEY_SERIALIZED_SIZE];
|
||||
addr.copy_from_slice(&sha3::Keccak256::digest(&pubkey.serialize()[1..])[12..]);
|
||||
assert_eq!(addr.len(), HASHED_PUBKEY_SERIALIZED_SIZE);
|
||||
addr
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug)]
|
||||
pub struct SecpSignatureOffsets {
|
||||
pub signature_offset: u16, // offset to [signature,recovery_id] of 64+1 bytes
|
||||
pub signature_instruction_index: u8,
|
||||
pub eth_address_offset: u16, // offset to eth_address of 20 bytes
|
||||
pub eth_address_instruction_index: u8,
|
||||
pub message_data_offset: u16, // offset to start of message data
|
||||
pub message_data_size: u16, // size of message data
|
||||
pub message_instruction_index: u8,
|
||||
}
|
||||
|
||||
fn get_data_slice<'a>(
|
||||
instruction_datas: &'a [&[u8]],
|
||||
instruction_index: u8,
|
||||
offset_start: u16,
|
||||
size: usize,
|
||||
) -> Result<&'a [u8], Secp256k1Error> {
|
||||
let signature_index = instruction_index as usize;
|
||||
if signature_index > instruction_datas.len() {
|
||||
return Err(Secp256k1Error::InvalidDataOffsets);
|
||||
}
|
||||
let signature_instruction = &instruction_datas[signature_index];
|
||||
let start = offset_start as usize;
|
||||
let end = start + size;
|
||||
if end > signature_instruction.len() {
|
||||
return Err(Secp256k1Error::InvalidSignature);
|
||||
}
|
||||
|
||||
Ok(&instruction_datas[signature_index][start..end])
|
||||
}
|
||||
|
||||
pub fn verify_eth_addresses(
|
||||
data: &[u8],
|
||||
instruction_datas: &[&[u8]],
|
||||
) -> Result<(), Secp256k1Error> {
|
||||
if data.is_empty() {
|
||||
return Err(Secp256k1Error::InvalidInstructionDataSize);
|
||||
}
|
||||
let count = data[0] as usize;
|
||||
let expected_data_size = 1 + count * SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
||||
if data.len() < expected_data_size {
|
||||
return Err(Secp256k1Error::InvalidInstructionDataSize);
|
||||
}
|
||||
for i in 0..count {
|
||||
let start = 1 + i * SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
||||
let end = start + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
||||
|
||||
let offsets: SecpSignatureOffsets = bincode::deserialize(&data[start..end])
|
||||
.map_err(|_| Secp256k1Error::InvalidSignature)?;
|
||||
|
||||
// Parse out signature
|
||||
let signature_index = offsets.signature_instruction_index as usize;
|
||||
if signature_index > instruction_datas.len() {
|
||||
return Err(Secp256k1Error::InvalidInstructionDataSize);
|
||||
}
|
||||
let signature_instruction = instruction_datas[signature_index];
|
||||
let sig_start = offsets.signature_offset as usize;
|
||||
let sig_end = sig_start + SIGNATURE_SERIALIZED_SIZE;
|
||||
if sig_end >= signature_instruction.len() {
|
||||
return Err(Secp256k1Error::InvalidSignature);
|
||||
}
|
||||
let signature =
|
||||
secp256k1::Signature::parse_slice(&signature_instruction[sig_start..sig_end])
|
||||
.map_err(|_| Secp256k1Error::InvalidSignature)?;
|
||||
|
||||
let recovery_id = secp256k1::RecoveryId::parse(signature_instruction[sig_end])
|
||||
.map_err(|_| Secp256k1Error::InvalidRecoveryId)?;
|
||||
|
||||
// Parse out pubkey
|
||||
let eth_address_slice = get_data_slice(
|
||||
&instruction_datas,
|
||||
offsets.eth_address_instruction_index,
|
||||
offsets.eth_address_offset,
|
||||
HASHED_PUBKEY_SERIALIZED_SIZE,
|
||||
)?;
|
||||
|
||||
// Parse out message
|
||||
let message_slice = get_data_slice(
|
||||
&instruction_datas,
|
||||
offsets.message_instruction_index,
|
||||
offsets.message_data_offset,
|
||||
offsets.message_data_size as usize,
|
||||
)?;
|
||||
|
||||
let mut hasher = sha3::Keccak256::new();
|
||||
hasher.update(message_slice);
|
||||
let message_hash = hasher.finalize();
|
||||
|
||||
let pubkey = secp256k1::recover(
|
||||
&secp256k1::Message::parse_slice(&message_hash).unwrap(),
|
||||
&signature,
|
||||
&recovery_id,
|
||||
)
|
||||
.map_err(|_| Secp256k1Error::InvalidSignature)?;
|
||||
let eth_address = construct_eth_pubkey(&pubkey);
|
||||
|
||||
if eth_address_slice != eth_address {
|
||||
return Err(Secp256k1Error::InvalidSignature);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
1
sdk/src/secp256k1_program.rs
Normal file
1
sdk/src/secp256k1_program.rs
Normal file
@ -0,0 +1 @@
|
||||
solana_sdk::declare_id!("KeccakSecp256k11111111111111111111111111111");
|
@ -1,6 +1,7 @@
|
||||
//! Defines a Transaction type to package an atomic sequence of instructions.
|
||||
|
||||
use crate::sanitize::{Sanitize, SanitizeError};
|
||||
use crate::secp256k1::verify_eth_addresses;
|
||||
use crate::{
|
||||
hash::Hash,
|
||||
instruction::{CompiledInstruction, Instruction, InstructionError},
|
||||
@ -330,6 +331,28 @@ impl Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_precompiles(&self) -> Result<()> {
|
||||
for instruction in &self.message().instructions {
|
||||
// The Transaction may not be sanitized at this point
|
||||
if instruction.program_id_index as usize >= self.message().account_keys.len() {
|
||||
return Err(TransactionError::AccountNotFound);
|
||||
}
|
||||
let program_id = &self.message().account_keys[instruction.program_id_index as usize];
|
||||
if crate::secp256k1_program::check_id(program_id) {
|
||||
let instruction_datas: Vec<_> = self
|
||||
.message()
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|instruction| instruction.data.as_ref())
|
||||
.collect();
|
||||
let data = &instruction.data;
|
||||
let e = verify_eth_addresses(data, &instruction_datas);
|
||||
e.map_err(|_| TransactionError::InvalidAccountIndex)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the positions of the pubkeys in `account_keys` associated with signing keypairs
|
||||
pub fn get_signing_keypair_positions(&self, pubkeys: &[Pubkey]) -> Result<Vec<Option<usize>>> {
|
||||
if self.message.account_keys.len() < self.message.header.num_required_signatures as usize {
|
||||
|
Reference in New Issue
Block a user