Skip to content

Commit 170e6f1

Browse files
lang: Hash at compile time (#63)
1 parent 60958eb commit 170e6f1

File tree

5 files changed

+163
-28
lines changed

5 files changed

+163
-28
lines changed

lang/attribute/account/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ proc-macro2 = "1.0"
1515
quote = "1.0"
1616
syn = { version = "=1.0.57", features = ["full"] }
1717
anyhow = "1.0.32"
18-
anchor-syn = { path = "../../syn", version = "0.1.0" }
18+
anchor-syn = { path = "../../syn", version = "0.1.0", features = ["hash"] }

lang/attribute/account/src/lib.rs

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,26 @@ pub fn account(
2727
let account_strct = parse_macro_input!(input as syn::ItemStruct);
2828
let account_name = &account_strct.ident;
2929

30-
// Namespace the discriminator to prevent collisions.
31-
let discriminator_preimage = {
32-
if namespace == "" {
33-
format!("account:{}", account_name.to_string())
34-
} else {
35-
format!("{}:{}", namespace, account_name.to_string())
36-
}
30+
let discriminator: proc_macro2::TokenStream = {
31+
// Namespace the discriminator to prevent collisions.
32+
let discriminator_preimage = {
33+
if namespace == "" {
34+
format!("account:{}", account_name.to_string())
35+
} else {
36+
format!("{}:{}", namespace, account_name.to_string())
37+
}
38+
};
39+
let mut discriminator = [0u8; 8];
40+
discriminator.copy_from_slice(
41+
&anchor_syn::hash::hash(discriminator_preimage.as_bytes()).to_bytes()[..8],
42+
);
43+
format!("{:?}", discriminator).parse().unwrap()
3744
};
3845

3946
let coder = quote! {
4047
impl anchor_lang::AccountSerialize for #account_name {
4148
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
42-
// TODO: we shouldn't have to hash at runtime. However, rust
43-
// is not happy when trying to include solana-sdk from
44-
// the proc-macro crate.
45-
let mut discriminator = [0u8; 8];
46-
discriminator.copy_from_slice(
47-
&anchor_lang::solana_program::hash::hash(
48-
#discriminator_preimage.as_bytes(),
49-
).to_bytes()[..8],
50-
);
51-
52-
writer.write_all(&discriminator).map_err(|_| ProgramError::InvalidAccountData)?;
49+
writer.write_all(&#discriminator).map_err(|_| ProgramError::InvalidAccountData)?;
5350
AnchorSerialize::serialize(
5451
self,
5552
writer
@@ -62,18 +59,11 @@ pub fn account(
6259
impl anchor_lang::AccountDeserialize for #account_name {
6360

6461
fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
65-
let mut discriminator = [0u8; 8];
66-
discriminator.copy_from_slice(
67-
&anchor_lang::solana_program::hash::hash(
68-
#discriminator_preimage.as_bytes(),
69-
).to_bytes()[..8],
70-
);
71-
72-
if buf.len() < discriminator.len() {
62+
if buf.len() < #discriminator.len() {
7363
return Err(ProgramError::AccountDataTooSmall);
7464
}
7565
let given_disc = &buf[..8];
76-
if &discriminator != given_disc {
66+
if &#discriminator != given_disc {
7767
return Err(ProgramError::InvalidInstructionData);
7868
}
7969
Self::try_deserialize_unchecked(buf)

lang/syn/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ edition = "2018"
99

1010
[features]
1111
idl = []
12+
hash = []
1213
default = []
1314

1415
[dependencies]
@@ -19,3 +20,6 @@ anyhow = "1.0.32"
1920
heck = "0.3.1"
2021
serde = { version = "1.0.118", features = ["derive"] }
2122
serde_json = "1.0"
23+
sha2 = "0.9.2"
24+
thiserror = "1.0"
25+
bs58 = "0.3.1"

lang/syn/src/hash.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Utility hashing module copied from `solana_program::program::hash`, since we
2+
// can't import solana_program for compile time hashing for some reason.
3+
4+
use serde::{Deserialize, Serialize};
5+
use sha2::{Digest, Sha256};
6+
use std::{convert::TryFrom, fmt, mem, str::FromStr};
7+
use thiserror::Error;
8+
9+
pub const HASH_BYTES: usize = 32;
10+
#[derive(Serialize, Deserialize, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
11+
#[repr(transparent)]
12+
pub struct Hash(pub [u8; HASH_BYTES]);
13+
14+
#[derive(Clone, Default)]
15+
pub struct Hasher {
16+
hasher: Sha256,
17+
}
18+
19+
impl Hasher {
20+
pub fn hash(&mut self, val: &[u8]) {
21+
self.hasher.update(val);
22+
}
23+
pub fn hashv(&mut self, vals: &[&[u8]]) {
24+
for val in vals {
25+
self.hash(val);
26+
}
27+
}
28+
pub fn result(self) -> Hash {
29+
// At the time of this writing, the sha2 library is stuck on an old version
30+
// of generic_array (0.9.0). Decouple ourselves with a clone to our version.
31+
Hash(<[u8; HASH_BYTES]>::try_from(self.hasher.finalize().as_slice()).unwrap())
32+
}
33+
}
34+
35+
impl AsRef<[u8]> for Hash {
36+
fn as_ref(&self) -> &[u8] {
37+
&self.0[..]
38+
}
39+
}
40+
41+
impl fmt::Debug for Hash {
42+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43+
write!(f, "{}", bs58::encode(self.0).into_string())
44+
}
45+
}
46+
47+
impl fmt::Display for Hash {
48+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49+
write!(f, "{}", bs58::encode(self.0).into_string())
50+
}
51+
}
52+
53+
#[derive(Debug, Clone, PartialEq, Eq, Error)]
54+
pub enum ParseHashError {
55+
#[error("string decoded to wrong size for hash")]
56+
WrongSize,
57+
#[error("failed to decoded string to hash")]
58+
Invalid,
59+
}
60+
61+
impl FromStr for Hash {
62+
type Err = ParseHashError;
63+
64+
fn from_str(s: &str) -> Result<Self, Self::Err> {
65+
let bytes = bs58::decode(s)
66+
.into_vec()
67+
.map_err(|_| ParseHashError::Invalid)?;
68+
if bytes.len() != mem::size_of::<Hash>() {
69+
Err(ParseHashError::WrongSize)
70+
} else {
71+
Ok(Hash::new(&bytes))
72+
}
73+
}
74+
}
75+
76+
impl Hash {
77+
pub fn new(hash_slice: &[u8]) -> Self {
78+
Hash(<[u8; HASH_BYTES]>::try_from(hash_slice).unwrap())
79+
}
80+
81+
pub const fn new_from_array(hash_array: [u8; HASH_BYTES]) -> Self {
82+
Self(hash_array)
83+
}
84+
85+
/// unique Hash for tests and benchmarks.
86+
pub fn new_unique() -> Self {
87+
use std::sync::atomic::{AtomicU64, Ordering};
88+
static I: AtomicU64 = AtomicU64::new(1);
89+
90+
let mut b = [0u8; HASH_BYTES];
91+
let i = I.fetch_add(1, Ordering::Relaxed);
92+
b[0..8].copy_from_slice(&i.to_le_bytes());
93+
Self::new(&b)
94+
}
95+
96+
pub fn to_bytes(self) -> [u8; HASH_BYTES] {
97+
self.0
98+
}
99+
}
100+
101+
/// Return a Sha256 hash for the given data.
102+
pub fn hashv(vals: &[&[u8]]) -> Hash {
103+
// Perform the calculation inline, calling this from within a program is
104+
// not supported
105+
#[cfg(not(target_arch = "bpf"))]
106+
{
107+
let mut hasher = Hasher::default();
108+
hasher.hashv(vals);
109+
hasher.result()
110+
}
111+
// Call via a system call to perform the calculation
112+
#[cfg(target_arch = "bpf")]
113+
{
114+
extern "C" {
115+
fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64;
116+
};
117+
let mut hash_result = [0; HASH_BYTES];
118+
unsafe {
119+
sol_sha256(
120+
vals as *const _ as *const u8,
121+
vals.len() as u64,
122+
&mut hash_result as *mut _ as *mut u8,
123+
);
124+
}
125+
Hash::new_from_array(hash_result)
126+
}
127+
}
128+
129+
/// Return a Sha256 hash for the given data.
130+
pub fn hash(val: &[u8]) -> Hash {
131+
hashv(&[val])
132+
}
133+
134+
/// Return the hash of the given hash extended with the given value.
135+
pub fn extend_and_hash(id: &Hash, val: &[u8]) -> Hash {
136+
let mut hash_data = id.as_ref().to_vec();
137+
hash_data.extend_from_slice(val);
138+
hash(&hash_data)
139+
}

lang/syn/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use quote::quote;
99
use std::collections::HashMap;
1010

1111
pub mod codegen;
12+
#[cfg(feature = "hash")]
13+
pub mod hash;
1214
#[cfg(feature = "idl")]
1315
pub mod idl;
1416
pub mod parser;

0 commit comments

Comments
 (0)