diff --git a/src/__tests__/integration/basic/vrf.test.ts b/src/__tests__/integration/basic/vrf.test.ts index c907be3..304fd64 100644 --- a/src/__tests__/integration/basic/vrf.test.ts +++ b/src/__tests__/integration/basic/vrf.test.ts @@ -138,11 +138,43 @@ describe('Testing the Profit Sharing Token', () => { await expect(pst.readState()).rejects.toThrow('Vrf verification failed.'); useWrongProof.pop(); }); + + it('should allow to test VRF on a standard forLocal Warp instance', async () => { + const localWarp = WarpFactory.forLocal(1823); + + const { contractTxId: vrfContractTxId } = await localWarp.createContract.deploy({ + wallet, + initState: JSON.stringify(initialState), + src: contractSrc + }); + + await mineBlock(localWarp); + const vrfContract = localWarp.contract(vrfContractTxId).connect(wallet); + await vrfContract.writeInteraction( + { + function: 'vrf' + }, + { vrf: true } + ); + const result = await vrfContract.readState(); + const lastTxId = Object.keys(result.cachedValue.validity).pop(); + const vrf = (result.cachedValue.state as any).vrf[lastTxId]; + + expect(vrf).not.toBeUndefined(); + expect(vrf['random_6_1'] == vrf['random_6_2']).toBe(true); + expect(vrf['random_6_2'] == vrf['random_6_3']).toBe(true); + expect(vrf['random_12_1'] == vrf['random_12_2']).toBe(true); + expect(vrf['random_12_2'] == vrf['random_12_3']).toBe(true); + expect(vrf['random_46_1'] == vrf['random_46_2']).toBe(true); + expect(vrf['random_46_2'] == vrf['random_46_3']).toBe(true); + expect(vrf['random_99_1'] == vrf['random_99_2']).toBe(true); + expect(vrf['random_99_2'] == vrf['random_99_3']).toBe(true); + }); }); class VrfDecorator extends ArweaveGatewayInteractionsLoader { constructor(protected readonly arweave: Arweave) { - super(arweave); + super(arweave, 'local'); } async load( diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 9ca9afe..fcb3709 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -209,7 +209,7 @@ export class HandlerBasedContract implements Contract { if (!this.signer) { throw new Error("Wallet not connected. Use 'connect' method first."); } - const { arweave, interactionsLoader } = this.warp; + const { arweave, interactionsLoader, environment } = this.warp; const effectiveTags = options?.tags || []; const effectiveTransfer = options?.transfer || emptyTransfer; @@ -228,7 +228,7 @@ export class HandlerBasedContract implements Contract { throw new Error('Ar Transfers are not allowed for bundled interactions'); } - if (effectiveVrf && !bundleInteraction) { + if (effectiveVrf && !bundleInteraction && environment === 'mainnet') { throw new Error('Vrf generation is only available for bundle interaction'); } @@ -245,7 +245,7 @@ export class HandlerBasedContract implements Contract { effectiveTransfer, effectiveStrict, false, - false, + effectiveVrf && environment !== 'mainnet', effectiveReward ); const response = await arweave.transactions.post(interactionTx); diff --git a/src/core/WarpBuilder.ts b/src/core/WarpBuilder.ts index 9246769..f487700 100644 --- a/src/core/WarpBuilder.ts +++ b/src/core/WarpBuilder.ts @@ -73,7 +73,9 @@ export class WarpBuilder { public useArweaveGateway(): WarpBuilder { this._definitionLoader = new ContractDefinitionLoader(this._arweave, new MemCache()); - this._interactionsLoader = new CacheableInteractionsLoader(new ArweaveGatewayInteractionsLoader(this._arweave)); + this._interactionsLoader = new CacheableInteractionsLoader( + new ArweaveGatewayInteractionsLoader(this._arweave, this._environment) + ); return this; } diff --git a/src/core/modules/impl/ArweaveGatewayInteractionsLoader.ts b/src/core/modules/impl/ArweaveGatewayInteractionsLoader.ts index 11ddfbc..d248404 100644 --- a/src/core/modules/impl/ArweaveGatewayInteractionsLoader.ts +++ b/src/core/modules/impl/ArweaveGatewayInteractionsLoader.ts @@ -9,11 +9,14 @@ import { import { Benchmark } from '../../../logging/Benchmark'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { ArweaveWrapper } from '../../../utils/ArweaveWrapper'; -import { sleep } from '../../../utils/utils'; +import { bufToBn, sleep } from '../../../utils/utils'; import { GW_TYPE, InteractionsLoader } from '../InteractionsLoader'; import { InteractionsSorter } from '../InteractionsSorter'; import { EvaluationOptions } from '../StateEvaluator'; import { LexicographicalInteractionsSorter } from './LexicographicalInteractionsSorter'; +import { WarpEnvironment } from '../../Warp'; +import elliptic from 'elliptic'; +import { Evaluate } from '@idena/vrf-js'; const MAX_REQUEST = 100; @@ -38,6 +41,10 @@ export function bundledTxsFilter(tx: GQLEdgeInterface) { return !tx.node.parent?.id && !tx.node.bundledIn?.id; } +const EC = new elliptic.ec('secp256k1'); +const key = EC.genKeyPair(); +const pubKeyS = key.getPublic(true, 'hex'); + export class ArweaveGatewayInteractionsLoader implements InteractionsLoader { private readonly logger = LoggerFactory.INST.create('ArweaveGatewayInteractionsLoader'); @@ -75,7 +82,7 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader { private readonly arweaveWrapper: ArweaveWrapper; private readonly sorter: InteractionsSorter; - constructor(protected readonly arweave: Arweave) { + constructor(protected readonly arweave: Arweave, private readonly environment: WarpEnvironment) { this.arweaveWrapper = new ArweaveWrapper(arweave); this.sorter = new LexicographicalInteractionsSorter(arweave); } @@ -163,7 +170,28 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader { time: loadingBenchmark.elapsed() }); - return sortedInteractions.map((i) => i.node); + const isLocalOrTestnetEnv = this.environment === 'local' || this.environment === 'testnet'; + return sortedInteractions.map((i) => { + const interaction = i.node; + if (isLocalOrTestnetEnv) { + if ( + interaction.tags.some((t) => { + return t.name == SmartWeaveTags.REQUEST_VRF && t.value === 'true'; + }) + ) { + const data = this.arweave.utils.stringToBuffer(interaction.sortKey); + const [index, proof] = Evaluate(key.getPrivate().toArray(), data); + interaction.vrf = { + index: this.arweave.utils.bufferTob64Url(index), + proof: this.arweave.utils.bufferTob64Url(proof), + bigint: bufToBn(index).toString(), + pubkey: pubKeyS + }; + } + } + + return interaction; + }); } private async loadPages(variables: GqlReqVariables) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b2eb272..d487138 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,6 +1,7 @@ /* eslint-disable */ import cloneDeep from 'lodash/cloneDeep'; import copy from 'fast-copy'; +import { Buffer } from 'redstone-isomorphic'; export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -59,3 +60,18 @@ export function stripTrailingSlash(str: string) { export function indent(callDepth: number) { return ''.padEnd(callDepth * 2, ' '); } + +export function bufToBn(buf: Buffer) { + const hex = []; + const u8 = Uint8Array.from(buf); + + u8.forEach(function (i) { + let h = i.toString(16); + if (h.length % 2) { + h = '0' + h; + } + hex.push(h); + }); + + return BigInt('0x' + hex.join('')); +}