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/idls/light_compressed_token.ts b/js/stateless.js/src/idls/light_compressed_token.ts index c9fcbd8c9b..1aa2415b3f 100644 --- a/js/stateless.js/src/idls/light_compressed_token.ts +++ b/js/stateless.js/src/idls/light_compressed_token.ts @@ -1549,128 +1549,23 @@ export type LightCompressedToken = { errors: [ { code: 6000; - name: 'PublicKeyAmountMissmatch'; - msg: 'public keys and amounts must be of same length'; + name: 'SignerCheckFailed'; + msg: 'Signer check failed'; }, { code: 6001; - name: 'ComputeInputSumFailed'; - msg: 'ComputeInputSumFailed'; + name: 'CreateTransferInstructionFailed'; + msg: 'Create transfer instruction failed'; }, { code: 6002; - name: 'ComputeOutputSumFailed'; - msg: 'ComputeOutputSumFailed'; + name: 'AccountNotFound'; + msg: 'Account not found'; }, { code: 6003; - name: 'ComputeCompressSumFailed'; - msg: 'ComputeCompressSumFailed'; - }, - { - code: 6004; - name: 'ComputeDecompressSumFailed'; - msg: 'ComputeDecompressSumFailed'; - }, - { - code: 6005; - name: 'SumCheckFailed'; - msg: 'SumCheckFailed'; - }, - { - code: 6006; - name: 'DecompressRecipientUndefinedForDecompress'; - msg: 'DecompressRecipientUndefinedForDecompress'; - }, - { - code: 6007; - name: 'CompressedPdaUndefinedForDecompress'; - msg: 'CompressedPdaUndefinedForDecompress'; - }, - { - code: 6008; - name: 'DeCompressAmountUndefinedForDecompress'; - msg: 'DeCompressAmountUndefinedForDecompress'; - }, - { - code: 6009; - name: 'CompressedPdaUndefinedForCompress'; - msg: 'CompressedPdaUndefinedForCompress'; - }, - { - code: 6010; - name: 'DeCompressAmountUndefinedForCompress'; - msg: 'DeCompressAmountUndefinedForCompress'; - }, - { - code: 6011; - name: 'DelegateSignerCheckFailed'; - msg: 'DelegateSignerCheckFailed'; - }, - { - code: 6012; - name: 'MintTooLarge'; - msg: 'Minted amount greater than u64::MAX'; - }, - { - code: 6013; - name: 'SplTokenSupplyMismatch'; - msg: 'SplTokenSupplyMismatch'; - }, - { - code: 6014; - name: 'HeapMemoryCheckFailed'; - msg: 'HeapMemoryCheckFailed'; - }, - { - code: 6015; - name: 'InstructionNotCallable'; - msg: 'The instruction is not callable'; - }, - { - code: 6016; - name: 'ArithmeticUnderflow'; - msg: 'ArithmeticUnderflow'; - }, - { - code: 6017; - name: 'HashToFieldError'; - msg: 'HashToFieldError'; - }, - { - code: 6018; - name: 'InvalidAuthorityMint'; - msg: 'Expected the authority to be also a mint authority'; - }, - { - code: 6019; - name: 'InvalidFreezeAuthority'; - msg: 'Provided authority is not the freeze authority'; - }, - { - code: 6020; - name: 'InvalidDelegateIndex'; - }, - { - code: 6021; - name: 'TokenPoolPdaUndefined'; - }, - { - code: 6022; - name: 'IsTokenPoolPda'; - msg: 'Compress or decompress recipient is the same account as the token pool pda.'; - }, - { - code: 6023; - name: 'InvalidTokenPoolPda'; - }, - { - code: 6024; - name: 'NoInputTokenAccountsProvided'; - }, - { - code: 6025; - name: 'NoInputsProvided'; + name: 'SerializationError'; + msg: 'Serialization error'; }, ]; }; @@ -3230,128 +3125,23 @@ export const IDL: LightCompressedToken = { errors: [ { code: 6000, - name: 'PublicKeyAmountMissmatch', - msg: 'public keys and amounts must be of same length', + name: 'SignerCheckFailed', + msg: 'Signer check failed', }, { code: 6001, - name: 'ComputeInputSumFailed', - msg: 'ComputeInputSumFailed', + name: 'CreateTransferInstructionFailed', + msg: 'Create transfer instruction failed', }, { code: 6002, - name: 'ComputeOutputSumFailed', - msg: 'ComputeOutputSumFailed', + name: 'AccountNotFound', + msg: 'Account not found', }, { code: 6003, - name: 'ComputeCompressSumFailed', - msg: 'ComputeCompressSumFailed', - }, - { - code: 6004, - name: 'ComputeDecompressSumFailed', - msg: 'ComputeDecompressSumFailed', - }, - { - code: 6005, - name: 'SumCheckFailed', - msg: 'SumCheckFailed', - }, - { - code: 6006, - name: 'DecompressRecipientUndefinedForDecompress', - msg: 'DecompressRecipientUndefinedForDecompress', - }, - { - code: 6007, - name: 'CompressedPdaUndefinedForDecompress', - msg: 'CompressedPdaUndefinedForDecompress', - }, - { - code: 6008, - name: 'DeCompressAmountUndefinedForDecompress', - msg: 'DeCompressAmountUndefinedForDecompress', - }, - { - code: 6009, - name: 'CompressedPdaUndefinedForCompress', - msg: 'CompressedPdaUndefinedForCompress', - }, - { - code: 6010, - name: 'DeCompressAmountUndefinedForCompress', - msg: 'DeCompressAmountUndefinedForCompress', - }, - { - code: 6011, - name: 'DelegateSignerCheckFailed', - msg: 'DelegateSignerCheckFailed', - }, - { - code: 6012, - name: 'MintTooLarge', - msg: 'Minted amount greater than u64::MAX', - }, - { - code: 6013, - name: 'SplTokenSupplyMismatch', - msg: 'SplTokenSupplyMismatch', - }, - { - code: 6014, - name: 'HeapMemoryCheckFailed', - msg: 'HeapMemoryCheckFailed', - }, - { - code: 6015, - name: 'InstructionNotCallable', - msg: 'The instruction is not callable', - }, - { - code: 6016, - name: 'ArithmeticUnderflow', - msg: 'ArithmeticUnderflow', - }, - { - code: 6017, - name: 'HashToFieldError', - msg: 'HashToFieldError', - }, - { - code: 6018, - name: 'InvalidAuthorityMint', - msg: 'Expected the authority to be also a mint authority', - }, - { - code: 6019, - name: 'InvalidFreezeAuthority', - msg: 'Provided authority is not the freeze authority', - }, - { - code: 6020, - name: 'InvalidDelegateIndex', - }, - { - code: 6021, - name: 'TokenPoolPdaUndefined', - }, - { - code: 6022, - name: 'IsTokenPoolPda', - msg: 'Compress or decompress recipient is the same account as the token pool pda.', - }, - { - code: 6023, - name: 'InvalidTokenPoolPda', - }, - { - code: 6024, - name: 'NoInputTokenAccountsProvided', - }, - { - code: 6025, - name: 'NoInputsProvided', + name: 'SerializationError', + msg: 'Serialization error', }, ], }; diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index 3b52688ca8..539319e628 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. @@ -12,13 +21,13 @@ import { getIndexOrAdd } from '../instruction'; * defaultTestStateTreeAccounts().merkleTree * @returns Derived address */ -export async function deriveAddress( +export function deriveAddress( seed: Uint8Array, merkleTreePubkey: PublicKey = defaultTestStateTreeAccounts().merkleTree, -): Promise { +): PublicKey { 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,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([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); + 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/programs/system/src/sdk/address.rs b/programs/system/src/sdk/address.rs index 2df88c281a..d78ea04b46 100644 --- a/programs/system/src/sdk/address.rs +++ b/programs/system/src/sdk/address.rs @@ -4,6 +4,7 @@ use anchor_lang::{err, solana_program::pubkey::Pubkey, Result}; 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]> { let hash = match hash_to_bn254_field_size_be( [merkle_tree_pubkey.to_bytes(), *seed].concat().as_slice(), 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()); + } +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index c3b051f3e4..ca03c4c50f 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -53,7 +53,19 @@ 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. 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 +85,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 ///