diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 313becb0ba..2404b6ce41 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -3069,6 +3069,7 @@ dependencies = [ name = "solana-bpf-rust-sha" version = "1.8.0" dependencies = [ + "blake3", "solana-program 1.8.0", ] diff --git a/programs/bpf/c/src/sha/sha.c b/programs/bpf/c/src/sha/sha.c old mode 100644 new mode 100755 index b61a14de8f..ffbe978070 --- a/programs/bpf/c/src/sha/sha.c +++ b/programs/bpf/c/src/sha/sha.c @@ -43,5 +43,24 @@ extern uint64_t entrypoint(const uint8_t *input) { sol_assert(0 == sol_memcmp(result, expected, KECCAK_RESULT_LENGTH)); } + // Blake3 + { + uint8_t result[BLAKE3_RESULT_LENGTH]; + uint8_t expected[] = {0xad, 0x5d, 0x97, 0x5b, 0xc2, 0xc7, 0x46, 0x19, + 0x31, 0xb4, 0x87, 0x5d, 0x19, 0x6, 0xc5, 0x36, + 0xf4, 0x97, 0xa8, 0x45, 0x55, 0xec, 0xaf, 0xf2, + 0x50, 0x70, 0xe3, 0xe2, 0x3d, 0xbe, 0x7, 0x8c}; + + uint8_t bytes1[] = {'G', 'a', 'g', 'g', 'a', 'b', 'l', 'a', + 'g', 'h', 'b', 'l', 'a', 'g', 'h', '!'}; + uint8_t bytes2[] = {'f', 'l', 'u', 'r', 'b', 'o', 's'}; + const SolBytes bytes[] = {{bytes1, SOL_ARRAY_SIZE(bytes1)}, + {bytes2, SOL_ARRAY_SIZE(bytes2)}}; + + sol_blake3(bytes, SOL_ARRAY_SIZE(bytes), result); + + sol_assert(0 == sol_memcmp(result, expected, BLAKE3_RESULT_LENGTH)); + } + return SUCCESS; } diff --git a/programs/bpf/rust/sha/Cargo.toml b/programs/bpf/rust/sha/Cargo.toml index 06685aa87c..17ee599856 100644 --- a/programs/bpf/rust/sha/Cargo.toml +++ b/programs/bpf/rust/sha/Cargo.toml @@ -10,6 +10,7 @@ documentation = "https://docs.rs/solana-bpf-rust-sha" edition = "2018" [dependencies] +blake3 = "0.3.7" solana-program = { path = "../../../../sdk/program", version = "=1.8.0" } [lib] diff --git a/programs/bpf/rust/sha/src/lib.rs b/programs/bpf/rust/sha/src/lib.rs old mode 100644 new mode 100755 index 6ee55cc9ab..3a44b8eed4 --- a/programs/bpf/rust/sha/src/lib.rs +++ b/programs/bpf/rust/sha/src/lib.rs @@ -19,12 +19,22 @@ fn test_keccak256_hasher() { assert_eq!(hashv(vals), hasher.result()); } +fn test_blake3_hasher() { + use solana_program::blake3::hashv; + let v0: &[u8] = b"Gaggablaghblagh!"; + let v1: &[u8] = b"flurbos!"; + let vals: &[&[u8]] = &[v0, v1]; + let hash = blake3::hash(&[v0, v1].concat()); + assert_eq!(hashv(vals).0, *hash.as_bytes()); +} + #[no_mangle] pub extern "C" fn entrypoint(_input: *mut u8) -> u64 { msg!("sha"); test_sha256_hasher(); test_keccak256_hasher(); + test_blake3_hasher(); 0 } @@ -39,5 +49,6 @@ mod test { fn test_sha() { test_sha256_hasher(); test_keccak256_hasher(); + test_blake3_hasher(); } } diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index 3e93ce4ed9..2feb5c3cf4 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -1282,7 +1282,7 @@ fn assert_instruction_count() { ("relative_call", 10), ("sanity", 174), ("sanity++", 174), - ("sha", 694), + ("sha", 1040), ("struct_pass", 8), ("struct_ret", 22), ]); @@ -1303,7 +1303,7 @@ fn assert_instruction_count() { ("solana_bpf_rust_param_passing", 46), ("solana_bpf_rust_rand", 498), ("solana_bpf_rust_sanity", 917), - ("solana_bpf_rust_sha", 29099), + ("solana_bpf_rust_sha", 32384), ]); } diff --git a/programs/bpf_loader/src/syscalls.rs b/programs/bpf_loader/src/syscalls.rs old mode 100644 new mode 100755 index 87e06a0c6c..0c42306760 --- a/programs/bpf_loader/src/syscalls.rs +++ b/programs/bpf_loader/src/syscalls.rs @@ -13,15 +13,15 @@ use solana_sdk::{ account::{Account, AccountSharedData, ReadableAccount}, account_info::AccountInfo, account_utils::StateMut, - bpf_loader, bpf_loader_deprecated, + blake3, bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::Clock, entrypoint::{MAX_PERMITTED_DATA_INCREASE, SUCCESS}, epoch_schedule::EpochSchedule, feature_set::{ - cpi_data_cost, demote_sysvar_write_locks, enforce_aligned_host_addrs, - keccak256_syscall_enabled, memory_ops_syscalls, set_upgrade_authority_via_cpi_enabled, - sysvar_via_syscall, update_data_on_realloc, + blake3_syscall_enabled, cpi_data_cost, demote_sysvar_write_locks, + enforce_aligned_host_addrs, keccak256_syscall_enabled, memory_ops_syscalls, + set_upgrade_authority_via_cpi_enabled, sysvar_via_syscall, update_data_on_realloc, }, hash::{Hasher, HASH_BYTES}, ic_msg, @@ -134,6 +134,10 @@ pub fn register_syscalls( syscall_registry.register_syscall_by_name(b"sol_keccak256", SyscallKeccak256::call)?; } + if invoke_context.is_feature_active(&blake3_syscall_enabled::id()) { + syscall_registry.register_syscall_by_name(b"sol_blake3", SyscallBlake3::call)?; + } + if invoke_context.is_feature_active(&sysvar_via_syscall::id()) { syscall_registry .register_syscall_by_name(b"sol_get_clock_sysvar", SyscallGetClockSysvar::call)?; @@ -316,6 +320,16 @@ pub fn bind_syscall_context_objects<'a>( loader_id, }), ); + bind_feature_gated_syscall_context_object!( + vm, + invoke_context.is_feature_active(&blake3_syscall_enabled::id()), + Box::new(SyscallBlake3 { + base_cost: bpf_compute_budget.sha256_base_cost, + byte_cost: bpf_compute_budget.sha256_byte_cost, + compute_meter: invoke_context.get_compute_meter(), + loader_id, + }), + ); let is_sysvar_via_syscall_active = invoke_context.is_feature_active(&sysvar_via_syscall::id()); @@ -1329,6 +1343,65 @@ impl<'a> SyscallObject for SyscallMemset<'a> { } } +// Blake3 +pub struct SyscallBlake3<'a> { + base_cost: u64, + byte_cost: u64, + compute_meter: Rc>, + loader_id: &'a Pubkey, +} +impl<'a> SyscallObject for SyscallBlake3<'a> { + fn call( + &mut self, + vals_addr: u64, + vals_len: u64, + result_addr: u64, + _arg4: u64, + _arg5: u64, + memory_mapping: &MemoryMapping, + result: &mut Result>, + ) { + question_mark!(self.compute_meter.consume(self.base_cost), result); + let hash_result = question_mark!( + translate_slice_mut::( + memory_mapping, + result_addr, + blake3::HASH_BYTES as u64, + self.loader_id, + true, + ), + result + ); + let mut hasher = blake3::Hasher::default(); + if vals_len > 0 { + let vals = question_mark!( + translate_slice::<&[u8]>(memory_mapping, vals_addr, vals_len, self.loader_id, true), + result + ); + for val in vals.iter() { + let bytes = question_mark!( + translate_slice::( + memory_mapping, + val.as_ptr() as u64, + val.len() as u64, + self.loader_id, + true, + ), + result + ); + question_mark!( + self.compute_meter + .consume(self.byte_cost * (val.len() as u64 / 2)), + result + ); + hasher.hash(bytes); + } + } + hash_result.copy_from_slice(&hasher.result().to_bytes()); + *result = Ok(0); + } +} + // Cross-program invocation syscalls struct AccountReferences<'a> { diff --git a/sdk/bpf/c/inc/solana_sdk.h b/sdk/bpf/c/inc/solana_sdk.h old mode 100644 new mode 100755 index bb24e758e4..feaf569604 --- a/sdk/bpf/c/inc/solana_sdk.h +++ b/sdk/bpf/c/inc/solana_sdk.h @@ -457,6 +457,24 @@ uint64_t sol_keccak256( uint8_t *result ); +/** + * Length of a Blake3 hash result + */ +#define BLAKE3_RESULT_LENGTH 32 + +/** + * Blake3 + * + * @param bytes Array of byte arrays + * @param bytes_len Number of byte arrays + * @param result 32 byte array to hold the result + */ +uint64_t sol_blake3( + const SolBytes *bytes, + int bytes_len, + const uint8_t *result +); + /** * Account Meta */ diff --git a/sdk/program/Cargo.toml b/sdk/program/Cargo.toml old mode 100644 new mode 100755 index 92723aa249..d19ebbfdb0 --- a/sdk/program/Cargo.toml +++ b/sdk/program/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" [dependencies] bincode = "1.3.1" +blake3 = "0.3.7" borsh = "0.9.0" borsh-derive = "0.9.0" bs58 = "0.4.0" @@ -33,7 +34,6 @@ solana-sdk-macro = { path = "../macro", version = "=1.8.0" } thiserror = "1.0" [target.'cfg(not(target_arch = "bpf"))'.dependencies] -blake3 = "0.3.7" curve25519-dalek = "2.1.0" rand = "0.7.0" solana-logger = { path = "../../logger", version = "=1.8.0" } diff --git a/sdk/program/src/blake3.rs b/sdk/program/src/blake3.rs new file mode 100644 index 0000000000..d09da7d4af --- /dev/null +++ b/sdk/program/src/blake3.rs @@ -0,0 +1,219 @@ +//! The `blake3` module provides functions for creating hashes. +use crate::sanitize::Sanitize; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use std::{convert::TryFrom, fmt, mem, str::FromStr}; +use thiserror::Error; + +/// Size of hash +pub const HASH_BYTES: usize = 32; +/// Maximum string length of a base58 encoded hash +const MAX_BASE58_LEN: usize = 44; +#[derive( + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Clone, + Copy, + Default, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + AbiExample, +)] +#[repr(transparent)] +pub struct Hash(pub [u8; HASH_BYTES]); + +#[derive(Clone, Default)] +pub struct Hasher { + hasher: blake3::Hasher, +} + +impl Hasher { + pub fn hash(&mut self, val: &[u8]) { + self.hasher.update(val); + } + pub fn hashv(&mut self, vals: &[&[u8]]) { + for val in vals { + self.hash(val); + } + } + pub fn result(self) -> Hash { + Hash(*self.hasher.finalize().as_bytes()) + } +} + +impl Sanitize for Hash {} + +impl AsRef<[u8]> for Hash { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +impl fmt::Debug for Hash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", bs58::encode(self.0).into_string()) + } +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", bs58::encode(self.0).into_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum ParseHashError { + #[error("string decoded to wrong size for hash")] + WrongSize, + #[error("failed to decoded string to hash")] + Invalid, +} + +impl FromStr for Hash { + type Err = ParseHashError; + + fn from_str(s: &str) -> Result { + if s.len() > MAX_BASE58_LEN { + return Err(ParseHashError::WrongSize); + } + let bytes = bs58::decode(s) + .into_vec() + .map_err(|_| ParseHashError::Invalid)?; + if bytes.len() != mem::size_of::() { + Err(ParseHashError::WrongSize) + } else { + Ok(Hash::new(&bytes)) + } + } +} + +impl Hash { + pub fn new(hash_slice: &[u8]) -> Self { + Hash(<[u8; HASH_BYTES]>::try_from(hash_slice).unwrap()) + } + + pub const fn new_from_array(hash_array: [u8; HASH_BYTES]) -> Self { + Self(hash_array) + } + + /// unique Hash for tests and benchmarks. + pub fn new_unique() -> Self { + use std::sync::atomic::{AtomicU64, Ordering}; + static I: AtomicU64 = AtomicU64::new(1); + + let mut b = [0u8; HASH_BYTES]; + let i = I.fetch_add(1, Ordering::Relaxed); + b[0..8].copy_from_slice(&i.to_le_bytes()); + Self::new(&b) + } + + pub fn to_bytes(self) -> [u8; HASH_BYTES] { + self.0 + } +} + +/// Return a Blake3 hash for the given data. +pub fn hashv(vals: &[&[u8]]) -> Hash { + // Perform the calculation inline, calling this from within a program is + // not supported + #[cfg(not(target_arch = "bpf"))] + { + let mut hasher = Hasher::default(); + hasher.hashv(vals); + hasher.result() + } + // Call via a system call to perform the calculation + #[cfg(target_arch = "bpf")] + { + extern "C" { + fn sol_blake3(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64; + } + let mut hash_result = [0; HASH_BYTES]; + unsafe { + sol_blake3( + vals as *const _ as *const u8, + vals.len() as u64, + &mut hash_result as *mut _ as *mut u8, + ); + } + Hash::new_from_array(hash_result) + } +} + +/// Return a Blake3 hash for the given data. +pub fn hash(val: &[u8]) -> Hash { + hashv(&[val]) +} + +/// Return the hash of the given hash extended with the given value. +pub fn extend_and_hash(id: &Hash, val: &[u8]) -> Hash { + let mut hash_data = id.as_ref().to_vec(); + hash_data.extend_from_slice(val); + hash(&hash_data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_unique() { + assert!(Hash::new_unique() != Hash::new_unique()); + } + + #[test] + fn test_hash_fromstr() { + let hash = hash(&[1u8]); + + let mut hash_base58_str = bs58::encode(hash).into_string(); + + assert_eq!(hash_base58_str.parse::(), Ok(hash)); + + hash_base58_str.push_str(&bs58::encode(hash.0).into_string()); + assert_eq!( + hash_base58_str.parse::(), + Err(ParseHashError::WrongSize) + ); + + hash_base58_str.truncate(hash_base58_str.len() / 2); + assert_eq!(hash_base58_str.parse::(), Ok(hash)); + + hash_base58_str.truncate(hash_base58_str.len() / 2); + assert_eq!( + hash_base58_str.parse::(), + Err(ParseHashError::WrongSize) + ); + + let input_too_big = bs58::encode(&[0xffu8; HASH_BYTES + 1]).into_string(); + assert!(input_too_big.len() > MAX_BASE58_LEN); + assert_eq!( + input_too_big.parse::(), + Err(ParseHashError::WrongSize) + ); + + let mut hash_base58_str = bs58::encode(hash.0).into_string(); + assert_eq!(hash_base58_str.parse::(), Ok(hash)); + + // throw some non-base58 stuff in there + hash_base58_str.replace_range(..1, "I"); + assert_eq!( + hash_base58_str.parse::(), + Err(ParseHashError::Invalid) + ); + } + + #[test] + fn test_extend_and_hash() { + let val = "gHiljKpq"; + let val_hash = hash(val.as_bytes()); + let ext = "lM890t"; + let hash_ext = [&val_hash.0, ext.as_bytes()].concat(); + let ext_hash = extend_and_hash(&val_hash, ext.as_bytes()); + assert!(ext_hash == hash(&hash_ext)); + } +} diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs old mode 100644 new mode 100755 index 689c45d7d8..8bc14cf953 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -6,6 +6,7 @@ extern crate self as solana_program; pub mod account_info; +pub mod blake3; pub mod borsh; pub mod bpf_loader; pub mod bpf_loader_deprecated; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs old mode 100644 new mode 100755 index f6cf3f9067..a6a83600ec --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -147,6 +147,10 @@ pub mod system_transfer_zero_check { solana_sdk::declare_id!("BrTR9hzw4WBGFP65AJMbpAo64DcA3U6jdPSga9fMV5cS"); } +pub mod blake3_syscall_enabled { + solana_sdk::declare_id!("HTW2pSyErTj4BV6KBM9NZ9VBUJVxt7sacNWcf76wtzb3"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -184,6 +188,7 @@ lazy_static! { (memory_ops_syscalls::id(), "add syscalls for memory operations"), (add_missing_program_error_mappings::id(), "add missing program error mappings"), (system_transfer_zero_check::id(), "perform all checks for transfers of 0 lamports"), + (blake3_syscall_enabled::id(), "blake3 syscall"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()