Skip to content

Commit 33e0fe5

Browse files
committed
unified mint hashing
1 parent f702f3d commit 33e0fe5

File tree

6 files changed

+173
-169
lines changed

6 files changed

+173
-169
lines changed

program-libs/ctoken-types/src/instructions/extensions/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub use token_metadata::{TokenMetadataInstructionData, ZTokenMetadataInstruction
77

88
use crate::{
99
hash_cache::HashCache, state::Version, AnchorDeserialize, AnchorSerialize, CTokenError,
10+
HashableExtension,
1011
};
1112

1213
#[derive(Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)]
@@ -78,3 +79,18 @@ impl ZExtensionInstructionData<'_> {
7879
}
7980
}
8081
}
82+
83+
impl HashableExtension<CTokenError> for ZExtensionInstructionData<'_> {
84+
fn hash_with_hasher<H: Hasher>(
85+
&self,
86+
hashed_spl_mint: &[u8; 32],
87+
hash_cache: &mut HashCache,
88+
) -> Result<[u8; 32], CTokenError> {
89+
match self {
90+
ZExtensionInstructionData::TokenMetadata(token_metadata) => {
91+
token_metadata.hash_token_metadata::<H>(hashed_spl_mint, hash_cache)
92+
}
93+
_ => Err(CTokenError::UnsupportedExtension),
94+
}
95+
}
96+
}

program-libs/ctoken-types/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,19 @@ pub mod state;
1414
use anchor_lang::{AnchorDeserialize, AnchorSerialize};
1515
#[cfg(not(feature = "anchor"))]
1616
use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
17+
use hash_cache::HashCache;
18+
use light_hasher::Hasher;
1719
use light_macros::pubkey_array;
1820

21+
/// Trait for extension types that can be hashed with a given hasher
22+
pub trait HashableExtension<E> {
23+
fn hash_with_hasher<H: Hasher>(
24+
&self,
25+
hashed_spl_mint: &[u8; 32],
26+
hash_cache: &mut HashCache,
27+
) -> Result<[u8; 32], E>;
28+
}
29+
1930
pub const CPI_AUTHORITY: [u8; 32] = pubkey_array!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy");
2031
pub const COMPRESSED_TOKEN_PROGRAM_ID: [u8; 32] =
2132
pubkey_array!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m");

program-libs/ctoken-types/src/state/extensions/extension_struct.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use light_zero_copy::ZeroCopy;
33
use spl_pod::solana_msg::msg;
44

55
use crate::{
6+
hash_cache::HashCache,
67
state::extensions::{
78
CompressibleExtension, TokenMetadata, TokenMetadataConfig, ZTokenMetadataMut,
89
},
9-
AnchorDeserialize, AnchorSerialize, CTokenError,
10+
AnchorDeserialize, AnchorSerialize, CTokenError, HashableExtension,
1011
};
1112

1213
#[derive(Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)]
@@ -237,3 +238,23 @@ impl ZExtensionStructMut<'_> {
237238
}
238239
}
239240
}
241+
242+
impl HashableExtension<CTokenError> for ExtensionStruct {
243+
fn hash_with_hasher<H: Hasher>(
244+
&self,
245+
_hashed_spl_mint: &[u8; 32],
246+
_hash_cache: &mut HashCache,
247+
) -> Result<[u8; 32], CTokenError> {
248+
self.hash::<H>()
249+
}
250+
}
251+
252+
impl HashableExtension<CTokenError> for ZExtensionStructMut<'_> {
253+
fn hash_with_hasher<H: Hasher>(
254+
&self,
255+
_hashed_spl_mint: &[u8; 32],
256+
_hash_cache: &mut HashCache,
257+
) -> Result<[u8; 32], CTokenError> {
258+
self.hash::<H>()
259+
}
260+
}

program-libs/ctoken-types/src/state/mint.rs

Lines changed: 106 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -42,86 +42,120 @@ macro_rules! impl_compressed_mint_hash {
4242
$is_decompressed:expr
4343
$(,$deref_op:tt)?
4444
) => {{
45-
let hashed_spl_mint = $hash_cache.get_or_hash_mint(&$self.spl_mint.into())?;
46-
let mut supply_bytes = [0u8; 32];
47-
$self.supply
48-
.as_bytes()
49-
.iter()
50-
.rev()
51-
.zip(supply_bytes[24..].iter_mut())
52-
.for_each(|(x, y)| *y = *x);
45+
// Extract authority values with optional dereferencing
46+
let mint_authority = $self.mint_authority.as_ref().map(|auth| ($($deref_op)?auth).clone());
47+
let freeze_authority = $self.freeze_authority.as_ref().map(|auth| ($($deref_op)?auth).clone());
5348

54-
let hashed_mint_authority;
55-
let hashed_mint_authority_option =
56-
if let Some(mint_authority) = $self.mint_authority.as_ref() {
57-
hashed_mint_authority = $hash_cache.get_or_hash_pubkey(&($($deref_op)?mint_authority).to_bytes());
58-
Some(&hashed_mint_authority)
59-
} else {
60-
None
61-
};
62-
63-
let hashed_freeze_authority;
64-
let hashed_freeze_authority_option = if let Some(freeze_authority) =
65-
$self.freeze_authority.as_ref()
66-
{
67-
hashed_freeze_authority = $hash_cache.get_or_hash_pubkey(&($($deref_op)?freeze_authority).to_bytes());
68-
Some(&hashed_freeze_authority)
69-
} else {
70-
None
71-
};
72-
73-
let mint_hash = CompressedMint::hash_with_hashed_values(
74-
&hashed_spl_mint,
75-
&supply_bytes,
49+
// Call unified function
50+
compute_compressed_mint_hash_from_values(
51+
$self.spl_mint,
52+
$self.supply.as_bytes(),
7653
$self.decimals,
7754
$is_decompressed,
78-
&hashed_mint_authority_option,
79-
&hashed_freeze_authority_option,
55+
mint_authority,
56+
freeze_authority,
8057
$self.version,
81-
)?;
58+
$self.extensions.as_ref().map(|e| e.as_slice()),
59+
$hash_cache,
60+
)
61+
}};
62+
}
8263

83-
if let Some(extensions) = $self.extensions.as_ref() {
84-
let mut extension_hashchain = [0u8; 32];
85-
for extension in extensions.as_slice() {
86-
let extension_hash = if $self.version == 0 {
87-
extension.hash::<Poseidon>()?
88-
} else if $self.version == 1 {
89-
extension.hash::<Sha256BE>()?
90-
} else {
91-
return Err(CTokenError::InvalidTokenDataVersion);
92-
};
64+
/// Unified function to compute compressed mint hash from extracted values.
65+
/// This function contains the core logic that was previously duplicated
66+
/// in both the macro and instruction processing code.
67+
#[allow(clippy::too_many_arguments)]
68+
pub fn compute_compressed_mint_hash_from_values<ExtType, E>(
69+
spl_mint: light_compressed_account::Pubkey,
70+
supply_bytes: &[u8],
71+
decimals: u8,
72+
is_decompressed: bool,
73+
mint_authority: Option<light_compressed_account::Pubkey>,
74+
freeze_authority: Option<light_compressed_account::Pubkey>,
75+
version: u8,
76+
extensions: Option<&[ExtType]>,
77+
hash_cache: &mut HashCache,
78+
) -> Result<[u8; 32], E>
79+
where
80+
ExtType: crate::HashableExtension<E>,
81+
E: From<HasherError> + From<CTokenError>,
82+
{
83+
// 1. Get cached hashes for common values
84+
let hashed_spl_mint = hash_cache
85+
.get_or_hash_mint(&spl_mint.into())
86+
.map_err(E::from)?;
9387

94-
if $self.version == 0 {
95-
extension_hashchain = Poseidon::hashv(&[
96-
extension_hashchain.as_slice(),
97-
extension_hash.as_slice(),
98-
])?;
99-
} else if $self.version == 1 {
100-
extension_hashchain = Sha256BE::hashv(&[
101-
extension_hashchain.as_slice(),
102-
extension_hash.as_slice(),
103-
])?;
104-
} else {
105-
return Err(CTokenError::InvalidTokenDataVersion);
106-
}
107-
}
108-
if $self.version == 0 {
109-
Ok(Poseidon::hashv(&[
110-
mint_hash.as_slice(),
111-
extension_hashchain.as_slice(),
112-
])?)
113-
} else if $self.version == 1 {
114-
Ok(Sha256BE::hashv(&[
115-
mint_hash.as_slice(),
116-
extension_hashchain.as_slice(),
117-
])?)
88+
// 2. Convert supply bytes to 32-byte array (big-endian, right-aligned)
89+
let mut supply_bytes_32 = [0u8; 32];
90+
supply_bytes
91+
.iter()
92+
.rev()
93+
.zip(supply_bytes_32[24..].iter_mut())
94+
.for_each(|(x, y)| *y = *x);
95+
96+
// 3. Hash authorities if present
97+
let hashed_mint_authority;
98+
let hashed_mint_authority_option = if let Some(mint_authority) = mint_authority.as_ref() {
99+
hashed_mint_authority = hash_cache.get_or_hash_pubkey(&mint_authority.to_bytes());
100+
Some(&hashed_mint_authority)
101+
} else {
102+
None
103+
};
104+
105+
let hashed_freeze_authority;
106+
let hashed_freeze_authority_option = if let Some(freeze_authority) = freeze_authority.as_ref() {
107+
hashed_freeze_authority = hash_cache.get_or_hash_pubkey(&freeze_authority.to_bytes());
108+
Some(&hashed_freeze_authority)
109+
} else {
110+
None
111+
};
112+
113+
// 4. Compute base mint hash using existing function
114+
let mint_hash = CompressedMint::hash_with_hashed_values(
115+
&hashed_spl_mint,
116+
&supply_bytes_32,
117+
decimals,
118+
is_decompressed,
119+
&hashed_mint_authority_option,
120+
&hashed_freeze_authority_option,
121+
version,
122+
)
123+
.map_err(E::from)?;
124+
125+
// 5. Handle extensions if present
126+
if let Some(extensions) = extensions {
127+
let mut extension_hashchain = [0u8; 32];
128+
for extension in extensions {
129+
let extension_hash = if version == 0 {
130+
extension.hash_with_hasher::<Poseidon>(&hashed_spl_mint, hash_cache)
131+
} else if version == 1 {
132+
extension.hash_with_hasher::<Sha256BE>(&hashed_spl_mint, hash_cache)
118133
} else {
119-
return Err(CTokenError::InvalidTokenDataVersion);
120-
}
121-
} else {
122-
Ok(mint_hash)
134+
Err(CTokenError::InvalidTokenDataVersion.into())
135+
};
136+
extension_hashchain = match version {
137+
0 => {
138+
Poseidon::hashv(&[extension_hashchain.as_slice(), extension_hash?.as_slice()])?
139+
}
140+
1 => {
141+
Sha256BE::hashv(&[extension_hashchain.as_slice(), extension_hash?.as_slice()])?
142+
}
143+
_ => return Err(CTokenError::InvalidTokenDataVersion.into()),
144+
};
123145
}
124-
}};
146+
147+
// 6. Combine mint hash with extension hashchain
148+
let final_hash = if version == 0 {
149+
Poseidon::hashv(&[mint_hash.as_slice(), extension_hashchain.as_slice()])?
150+
} else if version == 1 {
151+
Sha256BE::hashv(&[mint_hash.as_slice(), extension_hashchain.as_slice()])?
152+
} else {
153+
return Err(CTokenError::InvalidTokenDataVersion.into());
154+
};
155+
Ok(final_hash)
156+
} else {
157+
Ok(mint_hash)
158+
}
125159
}
126160

127161
// TODO: unify code if possible
Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use anchor_compressed_token::ErrorCode;
22
use anchor_lang::prelude::ProgramError;
3-
use light_ctoken_types::{hash_cache::HashCache, state::ZExtensionStructMut};
4-
use light_hasher::{sha256::Sha256BE, Hasher, Poseidon};
5-
use pinocchio::{msg, pubkey::Pubkey};
3+
use light_ctoken_types::state::ZExtensionStructMut;
64

75
use crate::extensions::{token_metadata::create_output_token_metadata, ZExtensionInstructionData};
86

@@ -32,30 +30,3 @@ pub fn extensions_state_in_output_compressed_account(
3230
}
3331
Ok(())
3432
}
35-
36-
/// Creates extension hash chain for
37-
pub fn create_extension_hash_chain(
38-
extensions: &[ZExtensionInstructionData<'_>],
39-
hashed_spl_mint: &Pubkey,
40-
hash_cache: &mut HashCache,
41-
version: u8,
42-
) -> Result<[u8; 32], ProgramError> {
43-
let mut extension_hashchain = [0u8; 32];
44-
if version == 0 {
45-
for extension in extensions {
46-
let extension_hash = extension.hash::<Poseidon>(hashed_spl_mint, hash_cache)?;
47-
extension_hashchain =
48-
Poseidon::hashv(&[extension_hashchain.as_slice(), extension_hash.as_slice()])?;
49-
}
50-
} else if version == 1 {
51-
for extension in extensions {
52-
let extension_hash = extension.hash::<Sha256BE>(hashed_spl_mint, hash_cache)?;
53-
extension_hashchain =
54-
Sha256BE::hashv(&[extension_hashchain.as_slice(), extension_hash.as_slice()])?;
55-
}
56-
} else {
57-
msg!("Invalid version");
58-
return Err(ProgramError::InvalidInstructionData);
59-
}
60-
Ok(extension_hashchain)
61-
}

0 commit comments

Comments
 (0)