diff --git a/examples/name-service/programs/name-service/tests/test.rs b/examples/name-service/programs/name-service/tests/test.rs index 3839bbd03b..a597fe6766 100644 --- a/examples/name-service/programs/name-service/tests/test.rs +++ b/examples/name-service/programs/name-service/tests/test.rs @@ -67,11 +67,7 @@ async fn test_name_service() { address_queue_pubkey: env.address_merkle_tree_queue_pubkey, }; - let address_seed = derive_address_seed( - &[b"name-service", name.as_bytes()], - &name_service::ID, - &address_merkle_context, - ); + let address_seed = derive_address_seed(&[b"name-service", name.as_bytes()], &name_service::ID); let address = derive_address(&address_seed, &address_merkle_context); let address_merkle_context = diff --git a/js/stateless.js/src/actions/create-account.ts b/js/stateless.js/src/actions/create-account.ts index 73137f40e6..5e423c22ee 100644 --- a/js/stateless.js/src/actions/create-account.ts +++ b/js/stateless.js/src/actions/create-account.ts @@ -14,6 +14,7 @@ import { NewAddressParams, buildAndSignTx, deriveAddress, + deriveAddressSeed, sendAndConfirmTx, } from '../utils'; import { defaultTestStateTreeAccounts } from '../constants'; @@ -25,7 +26,7 @@ import { BN } from '@coral-xyz/anchor'; * * @param rpc RPC to use * @param payer Payer of the transaction and initialization fees - * @param seed Seed to derive the new account address + * @param seeds Seeds to derive the new account address * @param programId Owner of the new account * @param addressTree Optional address tree. Defaults to a current shared * address tree. @@ -40,7 +41,7 @@ import { BN } from '@coral-xyz/anchor'; export async function createAccount( rpc: Rpc, payer: Signer, - seed: Uint8Array, + seeds: Uint8Array[], programId: PublicKey, addressTree?: PublicKey, addressQueue?: PublicKey, @@ -52,8 +53,8 @@ export async function createAccount( addressTree = addressTree ?? defaultTestStateTreeAccounts().addressTree; addressQueue = addressQueue ?? defaultTestStateTreeAccounts().addressQueue; - /// TODO: enforce program-derived - const address = await deriveAddress(seed, addressTree); + const seed = deriveAddressSeed(seeds, programId); + const address = deriveAddress(seed, addressTree); const proof = await rpc.getValidityProofV0(undefined, [ { @@ -96,7 +97,7 @@ export async function createAccount( * * @param rpc RPC to use * @param payer Payer of the transaction and initialization fees - * @param seed Seed to derive the new account address + * @param seeds Seeds to derive the new account address * @param lamports Number of compressed lamports to initialize the * account with * @param programId Owner of the new account @@ -114,7 +115,7 @@ export async function createAccount( export async function createAccountWithLamports( rpc: Rpc, payer: Signer, - seed: Uint8Array, + seeds: Uint8Array[], lamports: number | BN, programId: PublicKey, addressTree?: PublicKey, @@ -138,8 +139,8 @@ export async function createAccountWithLamports( addressTree = addressTree ?? defaultTestStateTreeAccounts().addressTree; addressQueue = addressQueue ?? defaultTestStateTreeAccounts().addressQueue; - /// TODO: enforce program-derived - const address = await deriveAddress(seed, addressTree); + const seed = deriveAddressSeed(seeds, programId); + const address = deriveAddress(seed, addressTree); const proof = await rpc.getValidityProof( inputAccounts.map(account => bn(account.hash)), diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index dc9ad3087b..255a494199 100644 --- a/js/stateless.js/src/utils/address.ts +++ b/js/stateless.js/src/utils/address.ts @@ -1,27 +1,36 @@ 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. + * Derive an address for a compressed account from a seed and an address Merkle + * tree public key. * - * @param seed Seed to derive the address from - * @param merkleTreePubkey Merkle tree public key. Defaults to - * defaultTestStateTreeAccounts().merkleTree - * @returns Derived address + * @param seed Seed to derive the address from + * @param addressMerkleTreePubkey Merkle tree public key. Defaults to + * defaultTestStateTreeAccounts().addressTree + * @returns Derived address */ -export async function deriveAddress( +export function deriveAddress( seed: Uint8Array, - merkleTreePubkey: PublicKey = defaultTestStateTreeAccounts().merkleTree, -): Promise { + addressMerkleTreePubkey: PublicKey = defaultTestStateTreeAccounts().addressTree, +): PublicKey { if (seed.length != 32) { throw new Error('Seed length is not 32 bytes.'); } - const bytes = merkleTreePubkey.toBytes(); + const bytes = addressMerkleTreePubkey.toBytes(); const combined = Buffer.from([...bytes, ...seed]); - const hash = await hashToBn254FieldSizeBe(combined); + const hash = hashToBn254FieldSizeBe(combined); if (hash === null) { throw new Error('DeriveAddressError'); @@ -118,18 +127,71 @@ 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(32); - seed[0] = 1; - seed[1] = 2; - seed[2] = 3; - seed[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); + const derivedAddress = 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 = 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..57f2fa19b5 100644 --- a/js/stateless.js/src/utils/conversion.ts +++ b/js/stateless.js/src/utils/conversion.ts @@ -27,9 +27,23 @@ function isSmallerThanBn254FieldSizeBe(bytes: Buffer): boolean { return bigint.lt(FIELD_SIZE); } -export async function hashToBn254FieldSizeBe( - bytes: Buffer, -): Promise<[Buffer, number] | null> { +/** + * 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): [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 +65,24 @@ 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 { + const hasher = keccak_256.create(); + for (const input of bytes) { + hasher.update(input); + } + const 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/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index 29aba6bd26..44f689edd0 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -78,20 +78,24 @@ describe('compress', () => { await createAccount( rpc as TestRpc, payer, - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]), + [ + new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + ], LightSystemProgram.programId, ); await createAccountWithLamports( rpc as TestRpc, payer, - new Uint8Array([ - 1, 2, 255, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]), + [ + new Uint8Array([ + 1, 2, 255, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + ], 0, LightSystemProgram.programId, ); @@ -99,30 +103,37 @@ describe('compress', () => { await createAccount( rpc as TestRpc, payer, - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 1, - ]), + [ + new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 1, + ]), + ], LightSystemProgram.programId, ); await createAccount( rpc as TestRpc, payer, - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 2, - ]), + [ + new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 2, + ]), + ], LightSystemProgram.programId, ); await expect( createAccount( rpc as TestRpc, payer, - new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 2, - ]), + [ + new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 2, + ]), + ], LightSystemProgram.programId, ), ).rejects.toThrow(); @@ -169,10 +180,12 @@ describe('compress', () => { await createAccountWithLamports( rpc as TestRpc, payer, - new Uint8Array([ - 1, 255, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]), + [ + new Uint8Array([ + 1, 255, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + ], 100, LightSystemProgram.programId, ); diff --git a/js/stateless.js/tests/e2e/rpc-interop.test.ts b/js/stateless.js/tests/e2e/rpc-interop.test.ts index c528a0da1f..6c5fc53492 100644 --- a/js/stateless.js/tests/e2e/rpc-interop.test.ts +++ b/js/stateless.js/tests/e2e/rpc-interop.test.ts @@ -10,6 +10,7 @@ import { createAccountWithLamports, defaultTestStateTreeAccounts, deriveAddress, + deriveAddressSeed, sleep, } from '../../src'; import { getTestRpc, TestRpc } from '../../src/test-helpers/test-rpc'; @@ -107,12 +108,18 @@ describe('rpc-interop', () => { }); it('getValidityProof [noforester] (new-addresses) should match', async () => { - const newAddressSeed = new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 11, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]); + const newAddressSeeds = [ + new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 11, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]), + ]; + const newAddressSeed = deriveAddressSeed( + newAddressSeeds, + LightSystemProgram.programId, + ); - const newAddress = bn((await deriveAddress(newAddressSeed)).toBuffer()); + const newAddress = bn(deriveAddress(newAddressSeed).toBuffer()); /// consistent proof metadata for same address const validityProof = await rpc.getValidityProof([], [newAddress]); @@ -143,16 +150,18 @@ describe('rpc-interop', () => { }); /// Need a new unique address because the previous one has been created. - const newAddressSeedTest = new Uint8Array([ - 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 32, 29, 30, 31, 32, - ]); + const newAddressSeedsTest = [ + new Uint8Array([ + 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42, 42, 42, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 29, 30, 31, 32, + ]), + ]; /// Creates a compressed account with address using a (non-inclusion) /// 'validityProof' from Photon await createAccount( rpc, payer, - newAddressSeedTest, + newAddressSeedsTest, LightSystemProgram.programId, ); executedTxs++; @@ -162,7 +171,7 @@ describe('rpc-interop', () => { await createAccount( testRpc, payer, - newAddressSeed, + newAddressSeeds, LightSystemProgram.programId, ); executedTxs++; @@ -183,11 +192,17 @@ describe('rpc-interop', () => { // accounts are the same assert.isTrue(hash.eq(hashTest)); - const newAddressSeed = new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 42, 32, 42, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 32, 32, 27, 28, 29, 30, 31, 32, - ]); - const newAddress = bn((await deriveAddress(newAddressSeed)).toBytes()); + const newAddressSeeds = [ + new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 42, 32, 42, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 32, 32, 27, 28, 29, 30, 31, 32, + ]), + ]; + const newAddressSeed = deriveAddressSeed( + newAddressSeeds, + LightSystemProgram.programId, + ); + const newAddress = bn(deriveAddress(newAddressSeed).toBytes()); const validityProof = await rpc.getValidityProof([hash], [newAddress]); const validityProofTest = await testRpc.getValidityProof( @@ -277,10 +292,13 @@ describe('rpc-interop', () => { await createAccountWithLamports( rpc, payer, - new Uint8Array([ - 1, 2, 255, 4, 5, 6, 7, 8, 9, 10, 11, 111, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 29, 30, 31, 32, - ]), + [ + new Uint8Array([ + 1, 2, 255, 4, 5, 6, 7, 8, 9, 10, 11, 111, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 29, 30, 31, + 32, + ]), + ], 0, LightSystemProgram.programId, ); @@ -615,11 +633,12 @@ describe('rpc-interop', () => { }); it('[test-rpc missing] getCompressionSignaturesForAddress should work', async () => { - const seed = new Uint8Array(randomBytes(32)); + const seeds = [new Uint8Array(randomBytes(32))]; + const seed = deriveAddressSeed(seeds, LightSystemProgram.programId); const addressTree = defaultTestStateTreeAccounts().addressTree; - const address = await deriveAddress(seed, addressTree); + const address = deriveAddress(seed, addressTree); - await createAccount(rpc, payer, seed, LightSystemProgram.programId); + await createAccount(rpc, payer, seeds, LightSystemProgram.programId); // fetch the owners latest account const accounts = await rpc.getCompressedAccountsByOwner( @@ -639,15 +658,16 @@ describe('rpc-interop', () => { }); it('getCompressedAccount with address param should work ', async () => { - const seed = new Uint8Array(randomBytes(32)); + const seeds = [new Uint8Array(randomBytes(32))]; + const seed = deriveAddressSeed(seeds, LightSystemProgram.programId); const addressTree = defaultTestStateTreeAccounts().addressTree; const addressQueue = defaultTestStateTreeAccounts().addressQueue; - const address = await deriveAddress(seed, addressTree); + const address = deriveAddress(seed, addressTree); await createAccount( rpc, payer, - seed, + seeds, LightSystemProgram.programId, addressTree, addressQueue, diff --git a/macros/light-sdk-macros/src/accounts.rs b/macros/light-sdk-macros/src/accounts.rs index 19299e08c3..c40055c9c4 100644 --- a/macros/light-sdk-macros/src/accounts.rs +++ b/macros/light-sdk-macros/src/accounts.rs @@ -228,7 +228,6 @@ pub(crate) fn process_light_accounts(input: ItemStruct) -> Result { let address_seed = ::light_sdk::address::derive_address_seed( &#seeds, &crate::ID, - &unpacked_address_merkle_context, ); }); set_address_seed_calls.push(quote! { diff --git a/sdk/src/address.rs b/sdk/src/address.rs index a63418bedd..69c0f46eda 100644 --- a/sdk/src/address.rs +++ b/sdk/src/address.rs @@ -63,22 +63,14 @@ 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, - address_merkle_context: &AddressMerkleContext, -) -> [u8; 32] { - let mut inputs = Vec::with_capacity(seeds.len() + 2); +pub fn derive_address_seed(seeds: &[&[u8]], program_id: &Pubkey) -> [u8; 32] { + let mut inputs = Vec::with_capacity(seeds.len() + 1); let program_id = program_id.to_bytes(); inputs.push(program_id.as_slice()); - let merkle_tree_pubkey = address_merkle_context.address_merkle_tree_pubkey.to_bytes(); - inputs.push(merkle_tree_pubkey.as_slice()); - inputs.extend(seeds); let address = hashv_to_bn254_field_size_be(inputs.as_slice()); @@ -99,3 +91,52 @@ pub fn derive_address( // inside. 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()); + } +}