diff --git a/packages/cashscript/src/utils.ts b/packages/cashscript/src/utils.ts index e8a2744bf..f9ffc24ba 100644 --- a/packages/cashscript/src/utils.ts +++ b/packages/cashscript/src/utils.ts @@ -13,14 +13,12 @@ import { isHex, bigIntToCompactUint, NonFungibleTokenCapability, - bigIntToVmNumber, SigningSerializationFlag, } from '@bitauth/libauth'; import { encodeInt, hash160, hash256, - sha256, Op, Script, scriptToBytecode, @@ -419,6 +417,10 @@ export function getNetworkPrefix(network: string): 'bitcoincash' | 'bchtest' | ' const randomInt = (): bigint => BigInt(Math.floor(Math.random() * 10000)); +// Transaction ids, token categories and NFT commitments are random bytes, so tests that create many random UTXOs +// or tokens don't end up with colliding outpoints or categories +const randomBytesHex = (length: number): string => binToHex(crypto.getRandomValues(new Uint8Array(length))); + /** * Generate a random `Utxo` for use with `MockNetworkProvider.addUtxo` in tests and examples. * Fields can be overridden by passing a `defaults` object. @@ -427,7 +429,7 @@ const randomInt = (): bigint => BigInt(Math.floor(Math.random() * 10000)); * @returns A synthetic UTXO with random `txid`, `vout`, and `satoshis`. */ export const randomUtxo = >(defaults?: T): Utxo & T => ({ - txid: binToHex(sha256(bigIntToVmNumber(randomInt()))), + txid: randomBytesHex(32), vout: Math.floor(Math.random() * 10), satoshis: 100_000n + randomInt(), ...defaults, @@ -442,7 +444,7 @@ export const randomUtxo = >(defaults?: T): Utxo & T => ( */ export const randomToken = (defaults?: Partial): TokenDetails => ({ ...{ - category: binToHex(sha256(bigIntToVmNumber(randomInt()))), + category: randomBytesHex(32), amount: 100_000n + randomInt(), }, ...defaults, @@ -457,10 +459,10 @@ export const randomToken = (defaults?: Partial): TokenDetails => ( */ export const randomNFT = (defaults?: Partial): TokenDetails => ({ ...{ - category: binToHex(sha256(bigIntToVmNumber(randomInt()))), + category: randomBytesHex(32), amount: 0n, nft: { - commitment: binToHex(sha256(bigIntToVmNumber(randomInt()))).slice(0, 8), + commitment: randomBytesHex(4), capability: NonFungibleTokenCapability.none, }, }, diff --git a/packages/cashscript/test/util.test.ts b/packages/cashscript/test/util.test.ts index b09cb64c1..437b7574e 100644 --- a/packages/cashscript/test/util.test.ts +++ b/packages/cashscript/test/util.test.ts @@ -9,6 +9,9 @@ import { scriptToAddress, createUnlockingBytecode, getInputSize, + randomUtxo, + randomToken, + randomNFT, } from '../src/utils.js'; import { Network } from '../src/interfaces.js'; import { alicePkh, alicePub } from './fixture/vars.js'; @@ -115,4 +118,42 @@ describe('utils', () => { expect(regtestAddressP2sh20Tokens).toEqual(expectedRegtestAddressP2sh20Tokens); }); }); + + describe('random test helpers', () => { + // With the old 10,000 possible values, 2,000 draws were all but certain to collide + const draws = 2_000; + const distinct = (values: string[]): number => new Set(values).size; + + it('should give every random UTXO a distinct outpoint', () => { + const utxos = Array.from({ length: draws }, () => randomUtxo()); + + expect(distinct(utxos.map((utxo) => `${utxo.txid}:${utxo.vout}`))).toBe(draws); + utxos.forEach((utxo) => expect(utxo.txid).toMatch(/^[0-9a-f]{64}$/)); + }); + + it('should give every random token a distinct category', () => { + const categories = Array.from({ length: draws }, () => randomToken().category); + + expect(distinct(categories)).toBe(draws); + categories.forEach((category) => expect(category).toMatch(/^[0-9a-f]{64}$/)); + }); + + it('should give random NFTs distinct categories and 4-byte commitments', () => { + const nfts = Array.from({ length: draws }, () => randomNFT()); + const commitments = nfts.map((nft) => nft.nft!.commitment); + + expect(distinct(nfts.map((nft) => nft.category))).toBe(draws); + commitments.forEach((commitment) => expect(commitment).toMatch(/^[0-9a-f]{8}$/)); + // 4 random bytes can still collide by chance (~0.05% for 2,000 draws), so allow a few repeats + expect(distinct(commitments)).toBeGreaterThan(draws - 5); + }); + + it('should still let defaults override the random fields', () => { + const txid = 'ab'.repeat(32); + + expect(randomUtxo({ txid, vout: 3 })).toMatchObject({ txid, vout: 3 }); + expect(randomToken({ category: txid }).category).toBe(txid); + expect(randomNFT({ category: txid }).category).toBe(txid); + }); + }); });