diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index 3b52688ca8..2e4f54dba9 100644 --- a/js/stateless.js/src/utils/address.ts +++ b/js/stateless.js/src/utils/address.ts @@ -1,8 +1,17 @@ import { AccountMeta, PublicKey } from '@solana/web3.js'; -import { hashToBn254FieldSizeBe } from './conversion'; +import { hashToBn254FieldSizeBe, hashvToBn254FieldSizeBe } from './conversion'; import { defaultTestStateTreeAccounts } from '../constants'; import { getIndexOrAdd } from '../instruction'; +export function deriveAddressSeed( + seeds: Uint8Array[], + programId: PublicKey, +): Uint8Array { + const combinedSeeds: Uint8Array[] = [programId.toBytes(), ...seeds]; + const hash = hashvToBn254FieldSizeBe(combinedSeeds); + return hash; +} + /** * Derive an address for a compressed account from a seed and a merkle tree * public key. @@ -18,7 +27,7 @@ export async function deriveAddress( ): Promise { const bytes = merkleTreePubkey.toBytes(); const combined = Buffer.from([...bytes, ...seed]); - const hash = await hashToBn254FieldSizeBe(combined); + const hash = hashToBn254FieldSizeBe(combined); if (hash === null) { throw new Error('DeriveAddressError'); @@ -115,14 +124,63 @@ if (import.meta.vitest) { //@ts-ignore const { it, expect, describe } = import.meta.vitest; + const programId = new PublicKey('7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz'); + + describe('derive address seed', () => { + it('should derive a valid address seed', () => { + const seeds: Uint8Array[] = [ + new TextEncoder().encode("foo"), + new TextEncoder().encode("bar") + ]; + expect(deriveAddressSeed(seeds, programId)).toStrictEqual(new Uint8Array([ + 0, 246, 150, 3, 192, 95, 53, 123, 56, 139, 206, 179, 253, 133, 115, 103, 120, 155, + 251, 72, 250, 47, 117, 217, 118, 59, 174, 207, 49, 101, 201, 110 + ])); + }); + + it('should derive a valid address seed', () => { + const seeds: Uint8Array[] = [ + new TextEncoder().encode("ayy"), + new TextEncoder().encode("lmao") + ]; + expect(deriveAddressSeed(seeds, programId)).toStrictEqual(new Uint8Array([ + 0, 202, 44, 25, 221, 74, 144, 92, 69, 168, 38, 19, 206, 208, 29, 162, 53, 27, 120, + 214, 152, 116, 15, 107, 212, 168, 33, 121, 187, 10, 76, 233 + ])); + }); + }); + describe('deriveAddress function', () => { it('should derive a valid address from a seed and a merkle tree public key', async () => { - const seed = new Uint8Array([1, 2, 3, 4]); + const seeds: Uint8Array[] = [ + new TextEncoder().encode("foo"), + new TextEncoder().encode("bar") + ]; + const seed = deriveAddressSeed(seeds, programId); + const merkleTreePubkey = new PublicKey( + '11111111111111111111111111111111', + ); + const derivedAddress = await deriveAddress(seed, merkleTreePubkey); + expect(derivedAddress).toBeInstanceOf(PublicKey); + expect(derivedAddress).toStrictEqual(new PublicKey( + '139uhyyBtEh4e1CBDJ68ooK5nCeWoncZf9HPyAfRrukA' + )); + }); + + it('should derive a valid address from a seed and a merkle tree public key', async () => { + const seeds: Uint8Array[] = [ + new TextEncoder().encode("ayy"), + new TextEncoder().encode("lmao") + ]; + const seed = deriveAddressSeed(seeds, programId); const merkleTreePubkey = new PublicKey( '11111111111111111111111111111111', ); const derivedAddress = await deriveAddress(seed, merkleTreePubkey); expect(derivedAddress).toBeInstanceOf(PublicKey); + expect(derivedAddress).toStrictEqual(new PublicKey( + '12bhHm6PQjbNmEn3Yu1Gq9k7XwVn2rZpzYokmLwbFazN' + )); }); }); diff --git a/js/stateless.js/src/utils/conversion.ts b/js/stateless.js/src/utils/conversion.ts index d5fd6f8b75..88b2c4d882 100644 --- a/js/stateless.js/src/utils/conversion.ts +++ b/js/stateless.js/src/utils/conversion.ts @@ -27,9 +27,25 @@ function isSmallerThanBn254FieldSizeBe(bytes: Buffer): boolean { return bigint.lt(FIELD_SIZE); } -export async function hashToBn254FieldSizeBe( +/** + * Hash the provided `bytes` with Keccak256 and ensure the result fits in the + * BN254 prime field by repeatedly hashing the inputs with various "bump seeds" + * and truncating the resulting hash to 31 bytes. + * + * @deprecated Use `hashvToBn254FieldSizeBe` instead. + */ +export function hashToBn254FieldSizeBe( bytes: Buffer, -): Promise<[Buffer, number] | null> { +): [Buffer, number] | null { + // TODO(vadorovsky, affects-onchain): Get rid of the bump mechanism, it + // makes no sense. Doing the same as in the `hashvToBn254FieldSizeBe` below + // - overwriting the most significant byte with zero - is sufficient for + // truncation, it's also faster, doesn't force us to return `Option` and + // care about handling an error which is practically never returned. + // + // The reason we can't do it now is that it would affect on-chain programs. + // Once we can update programs, we can get rid of the seed bump (or even of + // this function all together in favor of the `hashv` variant). let bumpSeed = 255; while (bumpSeed >= 0) { const inputWithBumpSeed = Buffer.concat([ @@ -51,6 +67,26 @@ export async function hashToBn254FieldSizeBe( return null; } +/** + * Hash the provided `bytes` with Keccak256 and ensure that the result fits in + * the BN254 prime field by truncating the resulting hash to 31 bytes. + * + * @param bytes Input bytes + * + * @returns Hash digest + */ +export function hashvToBn254FieldSizeBe( + bytes: Uint8Array[], +): Uint8Array { + let hasher = keccak_256.create(); + for (var input of bytes) { + hasher.update(input); + } + let hash = hasher.digest(); + hash[0] = 0; + return hash; +} + /** Mutates array in place */ export function pushUniqueItems(items: T[], map: T[]): void { items.forEach(item => { diff --git a/programs/system/src/invoke/append_state.rs b/programs/system/src/invoke/append_state.rs index 29ede1e741..41f4ec6eac 100644 --- a/programs/system/src/invoke/append_state.rs +++ b/programs/system/src/invoke/append_state.rs @@ -7,6 +7,7 @@ use anchor_lang::{ use light_hasher::Poseidon; use light_heap::{bench_sbf_end, bench_sbf_start}; use light_macros::heap_neutral; +#[allow(deprecated)] use light_utils::hash_to_bn254_field_size_be; use crate::{ @@ -151,7 +152,9 @@ pub fn create_cpi_accounts_and_instruction_data<'a>( }); hashed_merkle_tree = match hashed_pubkeys.iter().find(|x| x.0 == account_info.key()) { Some(hashed_merkle_tree) => hashed_merkle_tree.1, - None => { + None => + { + #[allow(deprecated)] hash_to_bn254_field_size_be(&account_info.key().to_bytes()) .unwrap() .0 @@ -212,6 +215,7 @@ pub fn create_cpi_accounts_and_instruction_data<'a>( { Some(hashed_owner) => hashed_owner.1, None => { + #[allow(deprecated)] let hashed_owner = hash_to_bn254_field_size_be(&account.compressed_account.owner.to_bytes()) .unwrap() diff --git a/programs/system/src/invoke/verify_state_proof.rs b/programs/system/src/invoke/verify_state_proof.rs index 9983a09b68..4606ee35e8 100644 --- a/programs/system/src/invoke/verify_state_proof.rs +++ b/programs/system/src/invoke/verify_state_proof.rs @@ -11,6 +11,7 @@ use light_concurrent_merkle_tree::zero_copy::ConcurrentMerkleTreeZeroCopy; use light_hasher::Poseidon; use light_indexed_merkle_tree::zero_copy::IndexedMerkleTreeZeroCopy; use light_macros::heap_neutral; +#[allow(deprecated)] use light_utils::hash_to_bn254_field_size_be; use light_verifier::{ verify_create_addresses_and_merkle_proof_zkp, verify_create_addresses_zkp, @@ -87,6 +88,7 @@ pub fn fetch_roots_address_merkle_tree< /// Merkle tree pubkeys should be ordered for efficiency. #[inline(never)] #[heap_neutral] +#[allow(deprecated)] #[allow(unused_mut)] pub fn hash_input_compressed_accounts<'a, 'b, 'c: 'info, 'info>( remaining_accounts: &'a [AccountInfo<'info>], diff --git a/programs/system/src/sdk/address.rs b/programs/system/src/sdk/address.rs index 2df88c281a..543c1ab00c 100644 --- a/programs/system/src/sdk/address.rs +++ b/programs/system/src/sdk/address.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use anchor_lang::{err, solana_program::pubkey::Pubkey, Result}; +#[allow(deprecated)] use light_utils::hash_to_bn254_field_size_be; use crate::{errors::SystemProgramError, NewAddressParams, NewAddressParamsPacked}; + pub fn derive_address(merkle_tree_pubkey: &Pubkey, seed: &[u8; 32]) -> Result<[u8; 32]> { + #[allow(deprecated)] let hash = match hash_to_bn254_field_size_be( [merkle_tree_pubkey.to_bytes(), *seed].concat().as_slice(), ) { diff --git a/programs/system/src/sdk/compressed_account.rs b/programs/system/src/sdk/compressed_account.rs index d152e1c07b..999e1ab611 100644 --- a/programs/system/src/sdk/compressed_account.rs +++ b/programs/system/src/sdk/compressed_account.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use anchor_lang::prelude::*; use light_hasher::{Hasher, Poseidon}; +#[allow(deprecated)] use light_utils::hash_to_bn254_field_size_be; #[derive(Debug, PartialEq, Default, Clone, AnchorSerialize, AnchorDeserialize)] @@ -162,6 +163,7 @@ impl CompressedAccount { &merkle_tree_pubkey: &Pubkey, leaf_index: &u32, ) -> Result<[u8; 32]> { + #[allow(deprecated)] self.hash_with_hashed_values::( &hash_to_bn254_field_size_be(&self.owner.to_bytes()) .unwrap() @@ -188,6 +190,7 @@ mod tests { /// 5. no address and no data /// 6. no address, no data, no lamports #[test] + #[allow(deprecated)] fn test_compressed_account_hash() { let owner = Keypair::new().pubkey(); let address = [1u8; 32]; diff --git a/sdk/src/address.rs b/sdk/src/address.rs index d482507d22..68ae26c5de 100644 --- a/sdk/src/address.rs +++ b/sdk/src/address.rs @@ -1,4 +1,5 @@ use anchor_lang::{solana_program::pubkey::Pubkey, AnchorDeserialize, AnchorSerialize}; +#[allow(deprecated)] use light_utils::{hash_to_bn254_field_size_be, hashv_to_bn254_field_size_be}; use crate::merkle_context::{AddressMerkleContext, RemainingAccounts}; @@ -63,7 +64,6 @@ pub fn pack_new_address_params( /// let address = derive_address( /// &[b"my_compressed_account"], /// &crate::ID, -/// &address_merkle_context, /// ); /// ``` pub fn derive_address_seed(seeds: &[&[u8]], program_id: &Pubkey) -> [u8; 32] { @@ -90,5 +90,55 @@ pub fn derive_address( // PANICS: Not being able to find the bump for truncating the hash is // practically impossible. Quite frankly, we should just remove that error // inside. + #[allow(deprecated)] hash_to_bn254_field_size_be(input.as_slice()).unwrap().0 } + +#[cfg(test)] +mod test { + use light_macros::pubkey; + + use super::*; + + #[test] + fn test_derive_address_seed() { + let program_id = pubkey!("7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz"); + + let address_seed = derive_address_seed(&[b"foo", b"bar"], &program_id); + assert_eq!( + address_seed, + [ + 0, 246, 150, 3, 192, 95, 53, 123, 56, 139, 206, 179, 253, 133, 115, 103, 120, 155, + 251, 72, 250, 47, 117, 217, 118, 59, 174, 207, 49, 101, 201, 110 + ] + ); + + let address_seed = derive_address_seed(&[b"ayy", b"lmao"], &program_id); + assert_eq!( + address_seed, + [ + 0, 202, 44, 25, 221, 74, 144, 92, 69, 168, 38, 19, 206, 208, 29, 162, 53, 27, 120, + 214, 152, 116, 15, 107, 212, 168, 33, 121, 187, 10, 76, 233 + ] + ); + } + + #[test] + fn test_derive_address() { + let address_merkle_context = AddressMerkleContext { + address_merkle_tree_pubkey: pubkey!("11111111111111111111111111111111"), + address_queue_pubkey: pubkey!("22222222222222222222222222222222222222222222"), + }; + let program_id = pubkey!("7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz"); + + let address_seed = derive_address_seed(&[b"foo", b"bar"], &program_id); + let address = derive_address(&address_seed, &address_merkle_context); + let expected_address = pubkey!("139uhyyBtEh4e1CBDJ68ooK5nCeWoncZf9HPyAfRrukA"); + assert_eq!(address, expected_address.to_bytes()); + + let address_seed = derive_address_seed(&[b"ayy", b"lmao"], &program_id); + let address = derive_address(&address_seed, &address_merkle_context); + let expected_address = pubkey!("12bhHm6PQjbNmEn3Yu1Gq9k7XwVn2rZpzYokmLwbFazN"); + assert_eq!(address, expected_address.to_bytes()); + } +} diff --git a/sdk/src/compressed_account.rs b/sdk/src/compressed_account.rs index 06f7b5604b..0b61487feb 100644 --- a/sdk/src/compressed_account.rs +++ b/sdk/src/compressed_account.rs @@ -4,6 +4,7 @@ use anchor_lang::prelude::{ AccountInfo, AnchorDeserialize, AnchorSerialize, ProgramError, Pubkey, Result, }; use light_hasher::{DataHasher, Discriminator, Hasher, Poseidon}; +#[allow(deprecated)] use light_utils::hash_to_bn254_field_size_be; use crate::{ @@ -477,6 +478,7 @@ impl CompressedAccount { &merkle_tree_pubkey: &Pubkey, leaf_index: &u32, ) -> Result<[u8; 32]> { + #[allow(deprecated)] self.hash_with_hashed_values::( &hash_to_bn254_field_size_be(&self.owner.to_bytes()) .unwrap() diff --git a/utils/src/lib.rs b/utils/src/lib.rs index c3b051f3e4..888c435d4e 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -53,7 +53,20 @@ pub fn is_smaller_than_bn254_field_size_be(bytes: &[u8; 32]) -> bool { bigint < ark_bn254::Fr::MODULUS.into() } +/// Hashes the provided `bytes` with Keccak256 and ensures the result fits +/// in the BN254 prime field by repeatedly hashing the inputs with various +/// "bump seeds" and truncating the resulting hash to 31 bytes. +#[deprecated(note = "use `hashv_to_bn254_field_size_be` instead")] pub fn hash_to_bn254_field_size_be(bytes: &[u8]) -> Option<([u8; 32], u8)> { + // TODO(vadorovsky, affects-onchain): Get rid of the bump mechanism, it + // makes no sense. Doing the same as in the `hashv_to_bn254_field_size_be` + // below - overwriting the most significant byte with zero - is sufficient + // for truncation, it's also faster, doesn't force us to return `Option` + // and care about handling an error which is practically never returned. + // + // The reason we can't do it now is that it would affect on-chain programs. + // Once we can update programs, we can get rid of the seed bump (or even of + // this function all together in favor of the `hashv` variant). let mut bump_seed = [u8::MAX]; // Loops with decreasing bump seed to find a valid hash which is less than // bn254 Fr modulo field size. @@ -73,10 +86,7 @@ pub fn hash_to_bn254_field_size_be(bytes: &[u8]) -> Option<([u8; 32], u8)> { } /// Hashes the provided `bytes` with Keccak256 and ensures the result fits -/// in the BN254 prime field by repeatedly hashing the inputs with various -/// "bump seeds" and truncating the resulting hash to 31 bytes. -/// -/// The attempted "bump seeds" are bytes from 255 to 0. +/// in the BN254 prime field by truncating the resulting hash to 31 bytes. /// /// # Examples ///