fix: extract VRF to a plugin #248

This commit is contained in:
ppedziwiatr
2023-04-25 10:52:36 +02:00
committed by just_ppe
parent 244a5d6059
commit e22d949485
13 changed files with 126 additions and 100 deletions

View File

@@ -23,10 +23,10 @@ jobs:
run: yarn test:integration:internal-writes
- name: Run integration tests [wasm]
run: yarn test:integration:wasm
- name: Run regression tests
run: yarn test:regression
- name: Test example rust contracts
run: yarn build:test:wasm
- name: Run regression tests
run: yarn test:regression
- name: Trigger integration tests
if: github.ref_name == 'main'
run: >

View File

@@ -21,6 +21,7 @@ import path from 'path';
import { PstContract } from '../contract/definition/bindings/ts/PstContract';
import { State } from '../contract/definition/bindings/ts/ContractState';
import { TheAnswerExtension } from './the-answer-plugin';
import { VRFPlugin } from 'warp-contracts-plugin-vrf';
jest.setTimeout(30000);
@@ -60,7 +61,7 @@ describe('Testing the Rust WASM Profit Sharing Token', () => {
LoggerFactory.INST.logLevel('debug', 'WASM:Rust');
//LoggerFactory.INST.logLevel('debug', 'WasmContractHandlerApi');
warp = WarpFactory.forLocal(1820).use(new DeployPlugin()).use(new TheAnswerExtension());
warp = WarpFactory.forLocal(1820).use(new DeployPlugin()).use(new TheAnswerExtension()).use(new VRFPlugin());
({ arweave } = warp);
arweaveWrapper = new ArweaveWrapper(warp);

View File

@@ -1,6 +1,6 @@
{
"name": "warp-contracts",
"version": "1.4.2",
"version": "1.4.3-beta.0",
"description": "An implementation of the SmartWeave smart contract protocol.",
"types": "./lib/types/index.d.ts",
"main": "./lib/cjs/index.js",
@@ -78,12 +78,10 @@
},
"homepage": "https://github.com/warp-contracts/warp#readme",
"dependencies": {
"@idena/vrf-js": "^1.0.1",
"archiver": "^5.3.0",
"arweave": "^1.12.4",
"async-mutex": "^0.4.0",
"bignumber.js": "9.1.1",
"elliptic": "^6.5.4",
"events": "3.3.0",
"fast-copy": "^3.0.0",
"level": "^8.0.0",
@@ -95,6 +93,7 @@
"warp-wasm-metering": "1.0.1"
},
"devDependencies": {
"@idena/vrf-js": "^1.0.1",
"@types/cheerio": "^0.22.30",
"@types/jest": "^28.1.6",
"@types/node": "^18.0.6",
@@ -103,6 +102,7 @@
"arlocal": "1.1.42",
"cheerio": "^1.0.0-rc.10",
"colors": "^1.4.0",
"elliptic": "^6.5.4",
"esbuild": "0.17.5",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
@@ -119,6 +119,7 @@
"typescript": "^4.9.5",
"warp-contracts-plugin-deploy": "^1.0.3",
"warp-contracts-plugin-vm2": "1.0.0",
"warp-contracts-plugin-vrf": "^1.0.3",
"ws": "^8.11.0"
},
"resolutions": {

View File

@@ -17,6 +17,7 @@ import { LoggerFactory } from '../../../logging/LoggerFactory';
import { ArweaveGatewayInteractionsLoader } from '../../../core/modules/impl/ArweaveGatewayInteractionsLoader';
import { LexicographicalInteractionsSorter } from '../../../core/modules/impl/LexicographicalInteractionsSorter';
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
import { VRFPlugin } from "warp-contracts-plugin-vrf";
const EC = new elliptic.ec('secp256k1');
const key = EC.genKeyPair();
@@ -65,7 +66,8 @@ describe('Testing the Profit Sharing Token', () => {
.useArweaveGateway()
.setInteractionsLoader(loader)
.build()
.use(new DeployPlugin());
.use(new DeployPlugin())
.use(new VRFPlugin());
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
walletAddress = await arweave.wallets.jwkToAddress(wallet);
@@ -142,7 +144,9 @@ describe('Testing the Profit Sharing Token', () => {
});
it('should allow to test VRF on a standard forLocal Warp instance', async () => {
const localWarp = WarpFactory.forLocal(1823).use(new DeployPlugin());
const localWarp = WarpFactory.forLocal(1823)
.use(new DeployPlugin())
.use(new VRFPlugin());
const { contractTxId: vrfContractTxId } = await localWarp.deploy({
wallet,
@@ -174,7 +178,9 @@ describe('Testing the Profit Sharing Token', () => {
});
it('should allow to test VRF on a standard forLocal Warp instance in strict mode', async () => {
const localWarp = WarpFactory.forLocal(1823).use(new DeployPlugin());
const localWarp = WarpFactory.forLocal(1823)
.use(new DeployPlugin())
.use(new VRFPlugin());
const { contractTxId: vrfContractTxId } = await localWarp.deploy({
wallet,
@@ -206,7 +212,9 @@ describe('Testing the Profit Sharing Token', () => {
});
it('should allow to test VRF on a standard forLocal Warp instance with internal writes', async () => {
const localWarp = WarpFactory.forLocal(1823).use(new DeployPlugin());
const localWarp = WarpFactory.forLocal(1823)
.use(new DeployPlugin())
.use(new VRFPlugin());
const { contractTxId: vrfContractTxId } = await localWarp.deploy({
wallet,

View File

@@ -31,7 +31,6 @@ import {
} from './Contract';
import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract';
import { InnerWritesEvaluator } from './InnerWritesEvaluator';
import { generateMockVrf } from '../utils/vrf';
import { CustomSignature, Signature } from './Signature';
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
@@ -40,6 +39,7 @@ import { TransactionStatusResponse } from '../utils/types/arweave-types';
import { InteractionState } from './states/InteractionState';
import { ContractInteractionState } from './states/ContractInteractionState';
import { Crypto } from 'warp-isomorphic';
import { VrfPluginFunctions } from '../core/WarpPlugin';
/**
* An implementation of {@link Contract} that is backwards compatible with current style
@@ -688,7 +688,12 @@ export class HandlerBasedContract<State> implements Contract<State> {
dummyTx.sortKey = await this._sorter.createSortKey(dummyTx.block.id, dummyTx.id, dummyTx.block.height, true);
dummyTx.strict = strict;
if (vrf) {
dummyTx.vrf = generateMockVrf(dummyTx.sortKey, arweave);
const vrfPlugin = this.warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
if (vrfPlugin) {
dummyTx.vrf = vrfPlugin.process().generateMockVrf(dummyTx.sortKey);
} else {
this.logger.warn('Cannot generate mock vrf for interaction - no "warp-contracts-plugin-vrf" attached!');
}
}
const handleResult = await this.evalInteraction<Input, View>(

View File

@@ -174,6 +174,14 @@ export class Warp {
return this.plugins.get(type) as WarpPlugin<P, Q>;
}
maybeLoadPlugin<P, Q>(type: WarpPluginType): WarpPlugin<P, Q> | null {
if (!this.hasPlugin(type)) {
return null;
}
return this.plugins.get(type) as WarpPlugin<P, Q>;
}
// Close cache connection
async close(): Promise<void> {
return Promise.all([

View File

@@ -1,3 +1,6 @@
import Arweave from 'arweave';
import { VrfData } from '../legacy/gqlResult';
export const knownWarpPluginsPartial = [`^smartweave-extension-`] as const;
export const knownWarpPlugins = [
'evm-signature-verification',
@@ -7,7 +10,8 @@ export const knownWarpPlugins = [
'fetch-options',
'deploy',
'contract-blacklist',
'vm2'
'vm2',
'vrf'
] as const;
type WarpPluginPartialType = `smartweave-extension-${string}`;
export type WarpKnownPluginType = (typeof knownWarpPlugins)[number];
@@ -18,3 +22,8 @@ export interface WarpPlugin<T, R> {
process(input: T): R;
}
export type VrfPluginFunctions = {
generateMockVrf(sortKey: string): VrfData;
verify(vrf: VrfData, sortKey: string): boolean;
};

View File

@@ -9,15 +9,16 @@ import { InteractionsSorter } from '../InteractionsSorter';
import { EvaluationOptions } from '../StateEvaluator';
import { LexicographicalInteractionsSorter } from './LexicographicalInteractionsSorter';
import { Warp, WarpEnvironment } from '../../Warp';
import { generateMockVrf } from '../../../utils/vrf';
import { Tag } from 'utils/types/arweave-types';
import { ArweaveGQLTxsFetcher } from './ArweaveGQLTxsFetcher';
import { WarpTags, WARP_TAGS } from '../../KnownTags';
import { safeParseInt } from '../../../utils/utils';
import { VrfPluginFunctions, WarpPlugin } from '../../WarpPlugin';
import { TagsParser } from './TagsParser';
const MAX_REQUEST = 100;
// SortKey.blockHeight is block height
// at which interaction was send to bundler
// at which interaction was sent to bundler
// it can be actually finalized in later block
// we assume that this maximal "delay"
const EMPIRIC_BUNDLR_FINALITY_TIME = 100;
@@ -44,7 +45,9 @@ export class ArweaveGatewayBundledInteractionLoader implements InteractionsLoade
private arweaveFetcher: ArweaveGQLTxsFetcher;
private arweaveWrapper: ArweaveWrapper;
private _warp: Warp;
private readonly sorter: InteractionsSorter;
private readonly tagsParser = new TagsParser();
constructor(protected readonly arweave: Arweave, private readonly environment: WarpEnvironment) {
this.sorter = new LexicographicalInteractionsSorter(arweave);
@@ -108,12 +111,13 @@ export class ArweaveGatewayBundledInteractionLoader implements InteractionsLoade
const sortedInteractions = await this.sorter.sort(interactions);
const isLocalOrTestnetEnv = this.environment === 'local' || this.environment === 'testnet';
const vrfPlugin = this._warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
return sortedInteractions
.filter((interaction) => this.isNewerThenSortKeyBlockHeight(interaction))
.filter((interaction) => this.isSortKeyInBounds(fromSortKey, toSortKey, interaction))
.map((interaction) => this.attachSequencerDataToInteraction(interaction))
.map((interaction) => this.maybeAddMockVrf(isLocalOrTestnetEnv, interaction))
.map((interaction) => this.maybeAddMockVrf(isLocalOrTestnetEnv, interaction, vrfPlugin))
.map((interaction, index, allInteractions) => this.verifySortKeyIntegrity(interaction, index, allInteractions))
.map(({ node: interaction }) => interaction);
}
@@ -218,14 +222,18 @@ export class ArweaveGatewayBundledInteractionLoader implements InteractionsLoade
return interactions;
}
private maybeAddMockVrf(isLocalOrTestnetEnv: boolean, interaction: GQLEdgeInterface): GQLEdgeInterface {
private maybeAddMockVrf(
isLocalOrTestnetEnv: boolean,
interaction: GQLEdgeInterface,
vrfPlugin?: WarpPlugin<void, VrfPluginFunctions>
): GQLEdgeInterface {
if (isLocalOrTestnetEnv) {
if (
interaction.node.tags.some((t) => {
return t.name == WARP_TAGS.REQUEST_VRF && t.value === 'true';
})
) {
interaction.node.vrf = generateMockVrf(interaction.node.sortKey, this.arweave);
if (this.tagsParser.hasVrfTag(interaction.node)) {
if (vrfPlugin) {
interaction.node.vrf = vrfPlugin.process().generateMockVrf(interaction.node.sortKey);
} else {
this.logger.warn('Cannot generate mock vrf for interaction - no "warp-contracts-plugin-vrf" attached!');
}
}
}
return interaction;
@@ -238,11 +246,7 @@ export class ArweaveGatewayBundledInteractionLoader implements InteractionsLoade
const sendToBundlerBlockHeight = Number.parseInt(blockHeightSortKey);
const finalizedBlockHeight = Number(interaction.node.block.height);
const blockHeightDiff = finalizedBlockHeight - sendToBundlerBlockHeight;
if (blockHeightDiff < 0) {
return false;
}
return true;
return blockHeightDiff >= 0;
}
return true;
}
@@ -263,5 +267,6 @@ export class ArweaveGatewayBundledInteractionLoader implements InteractionsLoade
set warp(warp: Warp) {
this.arweaveWrapper = new ArweaveWrapper(warp);
this.arweaveFetcher = new ArweaveGQLTxsFetcher(warp);
this._warp = warp;
}
}

View File

@@ -8,8 +8,9 @@ import { InteractionsSorter } from '../InteractionsSorter';
import { EvaluationOptions } from '../StateEvaluator';
import { LexicographicalInteractionsSorter } from './LexicographicalInteractionsSorter';
import { Warp, WarpEnvironment } from '../../Warp';
import { generateMockVrf } from '../../../utils/vrf';
import { ArweaveGQLTxsFetcher, ArweaveTransactionQuery } from './ArweaveGQLTxsFetcher';
import { VrfPluginFunctions } from '../../WarpPlugin';
import { TagsParser } from './TagsParser';
const MAX_REQUEST = 100;
@@ -22,6 +23,8 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
private readonly sorter: InteractionsSorter;
private arweaveTransactionQuery: ArweaveGQLTxsFetcher;
private _warp: Warp;
private readonly tagsParser = new TagsParser();
constructor(protected readonly arweave: Arweave, private readonly environment: WarpEnvironment) {
this.sorter = new LexicographicalInteractionsSorter(arweave);
@@ -76,9 +79,9 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
},
first: MAX_REQUEST
};
const innerWritesInteractions = await (
await this.arweaveTransactionQuery.transactions(innerWritesVariables)
).filter(bundledTxsFilter);
const innerWritesInteractions = (await this.arweaveTransactionQuery.transactions(innerWritesVariables)).filter(
bundledTxsFilter
);
this.logger.debug('Inner writes interactions length:', innerWritesInteractions.length);
interactions = interactions.concat(innerWritesInteractions);
@@ -116,15 +119,17 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
});
const isLocalOrTestnetEnv = this.environment === 'local' || this.environment === 'testnet';
const vrfPlugin = this._warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
return sortedInteractions.map((i) => {
const interaction = i.node;
if (isLocalOrTestnetEnv) {
if (
interaction.tags.some((t) => {
return t.name == WARP_TAGS.REQUEST_VRF && t.value === 'true';
})
) {
interaction.vrf = generateMockVrf(interaction.sortKey, this.arweave);
if (this.tagsParser.hasVrfTag(interaction)) {
if (vrfPlugin) {
interaction.vrf = vrfPlugin.process().generateMockVrf(interaction.sortKey);
} else {
this.logger.warn('Cannot generate mock vrf for interaction - no "warp-contracts-plugin-vrf" attached!');
}
}
}
@@ -142,5 +147,6 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
set warp(warp: Warp) {
this.arweaveTransactionQuery = new ArweaveGQLTxsFetcher(warp);
this._warp = warp;
}
}

View File

@@ -1,20 +1,24 @@
import Arweave from 'arweave';
import { ProofHoHash } from '@idena/vrf-js';
import elliptic from 'elliptic';
import { SortKeyCache, SortKeyCacheResult } from '../../../cache/SortKeyCache';
import { InteractionCall } from '../../ContractCallRecord';
import { ExecutionContext } from '../../../core/ExecutionContext';
import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier';
import { GQLNodeInterface, GQLTagInterface, VrfData } from '../../../legacy/gqlResult';
import { GQLNodeInterface, GQLTagInterface } from '../../../legacy/gqlResult';
import { Benchmark } from '../../../logging/Benchmark';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { indent } from '../../../utils/utils';
import { EvalStateResult, StateEvaluator } from '../StateEvaluator';
import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExecutorFactory';
import { TagsParser } from './TagsParser';
import { VrfPluginFunctions } from '../../WarpPlugin';
const EC = new elliptic.ec('secp256k1');
type EvaluationProgressInput = {
contractTxId: string;
currentInteraction: number;
allInteractions: number;
lastInteractionProcessingTime: string;
};
/**
* This class contains the base functionality of evaluating the contracts state - according
@@ -71,21 +75,11 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
const missingInteractionsLength = missingInteractions.length;
const evmSignatureVerificationPlugin = warp.hasPlugin('evm-signature-verification')
? warp.loadPlugin<GQLNodeInterface, Promise<boolean>>('evm-signature-verification')
: null;
const progressPlugin = warp.hasPlugin('evaluation-progress')
? warp.loadPlugin<
{
contractTxId: string;
currentInteraction: number;
allInteractions: number;
lastInteractionProcessingTime: string;
},
void
>('evaluation-progress')
: null;
const evmSignatureVerificationPlugin = warp.maybeLoadPlugin<GQLNodeInterface, Promise<boolean>>(
'evm-signature-verification'
);
const progressPlugin = warp.maybeLoadPlugin<EvaluationProgressInput, void>('evaluation-progress');
const vrfPlugin = warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
let shouldBreakAfterEvolve = false;
@@ -103,10 +97,14 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
currentSortKey = missingInteraction.sortKey;
if (missingInteraction.vrf) {
if (!this.verifyVrf(missingInteraction.vrf, missingInteraction.sortKey, this.arweave)) {
if (!vrfPlugin) {
this.logger.warn('Cannot verify vrf for interaction - no "warp-contracts-plugin-vrf" attached!');
} else {
if (!vrfPlugin.process().verify(missingInteraction.vrf, missingInteraction.sortKey)) {
throw new Error('Vrf verification failed.');
}
}
}
if (evmSignatureVerificationPlugin && this.tagsParser.isEvmSigned(missingInteraction)) {
try {
@@ -315,25 +313,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
return new SortKeyCacheResult(currentSortKey, evalStateResult);
}
private verifyVrf(vrf: VrfData, sortKey: string, arweave: Arweave): boolean {
const keys = EC.keyFromPublic(vrf.pubkey, 'hex');
let hash;
try {
// ProofHoHash throws its own 'invalid vrf' exception
hash = ProofHoHash(
keys.getPublic(),
arweave.utils.stringToBuffer(sortKey),
arweave.utils.b64UrlToBuffer(vrf.proof)
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
return false;
}
return arweave.utils.bufferTob64Url(hash) == vrf.index;
}
private logResult<State>(
result: InteractionResult<State, unknown>,
currentTx: GQLNodeInterface,

View File

@@ -112,4 +112,10 @@ export class TagsParser {
return false;
}
hasVrfTag(interaction: GQLNodeInterface) {
return interaction.tags.some((t) => {
return t.name == WARP_TAGS.REQUEST_VRF && t.value === 'true';
});
}
}

View File

@@ -1,20 +0,0 @@
import elliptic from 'elliptic';
import Arweave from 'arweave';
import { Evaluate } from '@idena/vrf-js';
import { bufToBn } from './utils';
import { VrfData } from '../legacy/gqlResult';
const EC = new elliptic.ec('secp256k1');
const key = EC.genKeyPair();
const pubKeyS = key.getPublic(true, 'hex');
export function generateMockVrf(sortKey: string, arweave: Arweave): VrfData {
const data = arweave.utils.stringToBuffer(sortKey);
const [index, proof] = Evaluate(key.getPrivate().toArray(), data);
return {
index: arweave.utils.bufferTob64Url(index),
proof: arweave.utils.bufferTob64Url(proof),
bigint: bufToBn(index).toString(),
pubkey: pubKeyS
};
}

View File

@@ -2365,7 +2365,17 @@ arweave-stream-tx@^1.1.0:
dependencies:
exponential-backoff "^3.1.0"
arweave@^1.10.13, arweave@^1.10.23, arweave@^1.10.5, arweave@^1.11.4, arweave@^1.12.4:
arweave@1.13.7:
version "1.13.7"
resolved "https://registry.yarnpkg.com/arweave/-/arweave-1.13.7.tgz#cda8534c833baec372a7052c61f53b4e39a886d7"
integrity sha512-Hv+x2bSI6UyBHpuVbUDMMpMje1ETfpJWj52kKfz44O0IqDRi/LukOkkDUptup1p6OT6KP1/DdpnUnsNHoskFeA==
dependencies:
arconnect "^0.4.2"
asn1.js "^5.4.1"
base64-js "^1.5.1"
bignumber.js "^9.0.2"
arweave@^1.10.13, arweave@^1.10.23, arweave@^1.10.5, arweave@^1.11.4:
version "1.13.0"
resolved "https://registry.yarnpkg.com/arweave/-/arweave-1.13.0.tgz#046d1b74efe5b157ad85279473f3729e8c7a47a9"
integrity sha512-pir1s0Lg7MzAxmFsPqht1SGOq8Hoj1fhf44/8PVjBd6T8l6JRrFN23UvKp7N+MXBYM5SqrLYB8KrgvxAaczGFg==
@@ -7479,6 +7489,14 @@ warp-contracts-plugin-vm2@1.0.0:
bignumber.js "^9.1.1"
vm2 "^3.9.16"
warp-contracts-plugin-vrf@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/warp-contracts-plugin-vrf/-/warp-contracts-plugin-vrf-1.0.3.tgz#84077e00b02778fe181c50ed4c2bf987d72c6ffe"
integrity sha512-QIrlN/HQ4FfaqiPl38auHtJYnvC15ID3GWzsEIukzHHdcKgskuzskc0q0fCCI63q74so++9EKbQ+0k4s1+XXtw==
dependencies:
"@idena/vrf-js" "^1.0.1"
elliptic "^6.5.4"
warp-isomorphic@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/warp-isomorphic/-/warp-isomorphic-1.0.0.tgz#dccccfc046bc6ac77919f8be1024ced1385c70ea"