feat: add support for vrf

This commit is contained in:
ppe
2022-05-13 15:42:35 +02:00
committed by just_ppe
parent 57b0d39229
commit 6036931cdb
11 changed files with 1226 additions and 582 deletions

View File

@@ -56,9 +56,11 @@
"homepage": "https://github.com/redstone-finance/redstone-smartweave#readme",
"dependencies": {
"@assemblyscript/loader": "^0.19.23",
"@idena/vrf-js": "^1.0.1",
"archiver": "^5.3.0",
"arweave": "1.10.23",
"bignumber.js": "^9.0.1",
"elliptic": "^6.5.4",
"fast-copy": "^2.1.1",
"knex": "^0.95.14",
"lodash": "^4.17.21",

View File

@@ -0,0 +1,191 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import {
ArweaveGatewayInteractionsLoader,
EvaluationOptions,
GQLEdgeInterface,
InteractionsLoader,
LexicographicalInteractionsSorter,
LoggerFactory,
PstContract,
PstState,
SmartWeave,
SmartWeaveNodeFactory
} from '@smartweave';
import path from 'path';
import { addFunds, mineBlock } from '../_helpers';
import { Evaluate } from '@idena/vrf-js';
import elliptic from 'elliptic';
const EC = new elliptic.ec('secp256k1');
const key = EC.genKeyPair();
const pubKeyS = key.getPublic(true, 'hex');
const useWrongIndex = [];
const useWrongProof = [];
describe('Testing the Profit Sharing Token', () => {
let contractSrc: string;
let wallet: JWKInterface;
let walletAddress: string;
let initialState: PstState;
let arweave: Arweave;
let arlocal: ArLocal;
let smartweave: SmartWeave;
let pst: PstContract;
let loader: InteractionsLoader;
beforeAll(async () => {
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1823, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1823,
protocol: 'http'
});
loader = new VrfDecorator(arweave);
LoggerFactory.INST.logLevel('error');
smartweave = SmartWeaveNodeFactory.memCachedBased(arweave)
.useArweaveGateway()
.setInteractionsLoader(loader)
.build();
wallet = await arweave.wallets.generate();
await addFunds(arweave, wallet);
walletAddress = await arweave.wallets.jwkToAddress(wallet);
contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
initialState = {
...stateFromFile,
...{
owner: walletAddress,
balances: {
...stateFromFile.balances,
[walletAddress]: 555669
}
}
};
const contractTxId = await smartweave.createContract.deploy({
wallet,
initState: JSON.stringify(initialState),
src: contractSrc
});
// connecting to the PST contract
pst = smartweave.pst(contractTxId);
// connecting wallet to the PST contract
pst.connect(wallet);
await mineBlock(arweave);
});
afterAll(async () => {
await arlocal.stop();
});
it('should properly return random numbers', async () => {
await pst.writeInteraction({
function: 'vrf'
});
await mineBlock(arweave);
const result = await pst.readState();
const lastTxId = Object.keys(result.validity).pop();
const vrf = (result.state as any).vrf[lastTxId];
console.log(vrf);
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);
});
it('should throw if random cannot be verified', async () => {
const txId = await pst.writeInteraction({
function: 'vrf'
});
await mineBlock(arweave);
useWrongIndex.push(txId);
await expect(pst.readState()).rejects.toThrow('Vrf verification failed.');
useWrongIndex.pop();
const txId2 = await pst.writeInteraction({
function: 'vrf'
});
await mineBlock(arweave);
useWrongProof.push(txId2);
await expect(pst.readState()).rejects.toThrow('Vrf verification failed.');
useWrongProof.pop();
});
});
class VrfDecorator extends ArweaveGatewayInteractionsLoader {
constructor(protected readonly arweave: Arweave) {
super(arweave);
}
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
const result = await super.load(contractId, fromBlockHeight, toBlockHeight, evaluationOptions);
const arUtils = this.arweave.utils;
const sorter = new LexicographicalInteractionsSorter(this.arweave);
for (const r of result) {
r.node.sortKey = await sorter.createSortKey(r.node.block.id, r.node.id, r.node.block.height);
const data = arUtils.stringToBuffer(r.node.sortKey);
const [index, proof] = Evaluate(key.getPrivate().toArray(), data);
r.node.vrf = {
index: useWrongIndex.includes(r.node.id)
? arUtils.bufferTob64Url(Uint8Array.of(1, 2, 3))
: arUtils.bufferTob64Url(index),
proof: useWrongProof.includes(r.node.id)
? 'pK5HGnXo_rJkZPJorIX7TBCAEikcemL2DgJaPB3Pfm2D6tZUdK9mDuBSRUkcHUDNnrO02O0-ogq1e32JVEuVvgR4i5YFa-UV9MEoHgHg4yv0e318WNfzNWPc9rlte7P7RoO57idHu5SSkm7Qj0f4pBjUR7lWODVKBYp9fEJ-PObZ'
: arUtils.bufferTob64Url(proof),
bigint: bufToBn(index).toString(),
pubkey: pubKeyS
};
}
return result;
}
}
function bufToBn(buf) {
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(''));
}

View File

@@ -34,7 +34,7 @@ export function handle(state, action) {
balances[target] = qty;
}
return { state };
return {state};
}
if (input.function === 'balance') {
@@ -49,7 +49,36 @@ export function handle(state, action) {
throw new ContractError('Cannot get balance, target does not exist');
}
return { result: { target, ticker, balance: balances[target] } };
return {result: {target, ticker, balance: balances[target]}};
}
if (input.function === 'vrf') {
if (!state.vrf) {
state.vrf = {};
}
state.vrf[SmartWeave.transaction.id] = {
vrf: SmartWeave.vrf.data,
value: SmartWeave.vrf.value,
random_6_1: SmartWeave.vrf.randomInt(6),
random_6_2: SmartWeave.vrf.randomInt(6),
random_6_3: SmartWeave.vrf.randomInt(6),
random_12_1: SmartWeave.vrf.randomInt(12),
random_12_2: SmartWeave.vrf.randomInt(12),
random_12_3: SmartWeave.vrf.randomInt(12),
random_46_1: SmartWeave.vrf.randomInt(46),
random_46_2: SmartWeave.vrf.randomInt(46),
random_46_3: SmartWeave.vrf.randomInt(46),
random_99_1: SmartWeave.vrf.randomInt(99),
random_99_2: SmartWeave.vrf.randomInt(99),
random_99_3: SmartWeave.vrf.randomInt(99),
}
return {state};
}
if (input.function === 'evolve' && canEvolve) {
@@ -59,7 +88,7 @@ export function handle(state, action) {
state.evolve = input.value;
return { state };
return {state};
}
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);

View File

@@ -144,13 +144,17 @@ export interface Contract<State = unknown> {
/**
* Creates a new "interaction" transaction using RedStone Sequencer - this, with combination with
* RedStone Gateway, gives instant transaction availability and finality guaranteed by Bundlr.
*
* @param input - new input to the contract that will be assigned with this interactions transaction
* @param tags - additional tags that can be attached to the newly created interaction transaction
* @param transfer - additional {@link ArTransfer} than can be attached to the interaction transaction
* @param strict - transaction will be posted on Arweave only if the dry-run of the input result is "ok"
* @param options
*/
bundleInteraction<Input = unknown>(input: Input, tags?: Tags, strict?: boolean): Promise<any | null>;
bundleInteraction<Input = unknown>(
input: Input,
options: {
tags?: Tags;
strict?: boolean;
vrf?: boolean;
}
): Promise<any | null>;
/**
* Returns the full call tree report the last

View File

@@ -36,6 +36,7 @@ import { NetworkInfoInterface } from 'arweave/node/network';
import stringify from 'safe-stable-stringify';
import * as crypto from 'crypto';
import Transaction from 'arweave/node/lib/transaction';
import { options } from 'tsconfig-paths/lib/options';
/**
* An implementation of {@link Contract} that is backwards compatible with current style
@@ -226,12 +227,19 @@ export class HandlerBasedContract<State> implements Contract<State> {
return interactionTx.id;
}
async bundleInteraction<Input>(input: Input, tags: Tags = [], strict = false): Promise<any | null> {
async bundleInteraction<Input>(
input: Input,
options: {
tags: [];
strict: false;
vrf: false;
}
): Promise<any | null> {
this.logger.info('Bundle interaction input', input);
if (!this.signer) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
const interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict);
const interactionTx = await this.createInteraction(input, options.tags, emptyTransfer, options.strict, options.vrf);
const response = await fetch(`${this._evaluationOptions.bundlerUrl}gateway/sequencer/register`, {
method: 'POST',
@@ -264,7 +272,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
input: Input,
tags: { name: string; value: string }[],
transfer: ArTransfer,
strict: boolean
strict: boolean,
vrf = false
) {
if (this._evaluationOptions.internalWrites) {
// Call contract and verify if there are any internal writes:
@@ -299,6 +308,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
}
if (vrf) {
tags.push({
name: SmartWeaveTags.REQUEST_VRF,
value: 'true'
});
}
const interactionTx = await createTx(
this.smartweave.arweave,
this.signer,

View File

@@ -15,5 +15,6 @@ export enum SmartWeaveTags {
INTERACT_WRITE = 'Interact-Write',
WASM_LANG = 'Wasm-Lang',
WASM_LANG_VERSION = 'Wasm-Lang-Version',
WASM_META = 'Wasm-Meta'
WASM_META = 'Wasm-Meta',
REQUEST_VRF = 'Request-Vrf'
}

View File

@@ -71,7 +71,7 @@ export class ArweaveGatewayInteractionsLoader implements InteractionsLoader {
private readonly arweaveWrapper: ArweaveWrapper;
constructor(private readonly arweave: Arweave) {
constructor(protected readonly arweave: Arweave) {
this.arweaveWrapper = new ArweaveWrapper(arweave);
}

View File

@@ -15,10 +15,16 @@ import {
InteractionResult,
LoggerFactory,
StateEvaluator,
TagsParser
TagsParser,
VrfData
} from '@smartweave';
import Arweave from 'arweave';
import { ProofHoHash } from '@idena/vrf-js';
import elliptic from 'elliptic';
const EC = new elliptic.ec('secp256k1');
/**
* This class contains the base functionality of evaluating the contracts state - according
* to the SmartWeave protocol.
@@ -78,6 +84,12 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
const interactionTx: GQLNodeInterface = missingInteraction.node;
if (interactionTx.vrf) {
if (!this.verifyVrf(interactionTx.vrf, interactionTx.sortKey, this.arweave)) {
throw new Error('Vrf verification failed.');
}
}
this.logger.debug(
`[${contractDefinition.txId}][${missingInteraction.node.id}][${missingInteraction.node.block.height}]: ${
missingInteractions.indexOf(missingInteraction) + 1
@@ -242,6 +254,24 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
return 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)
);
} catch (e: any) {
return false;
}
return arweave.utils.bufferTob64Url(hash) == vrf.index;
}
private logResult<State>(
result: InteractionResult<State, unknown>,
currentTx: GQLNodeInterface,

View File

@@ -51,6 +51,14 @@ export interface GQLNodeInterface {
confirmationStatus?: string;
source?: string;
bundlerTxId?: string;
vrf?: VrfData;
}
export interface VrfData {
index: string;
proof: string;
bigint: string;
pubkey: string;
}
export interface GQLEdgeInterface {

View File

@@ -1,7 +1,8 @@
/* eslint-disable */
import Arweave from 'arweave';
import { GQLNodeInterface, GQLTagInterface } from './gqlResult';
import {GQLNodeInterface, GQLTagInterface, VrfData} from './gqlResult';
import { EvaluationOptions } from '@smartweave/core';
import {kMaxLength} from "buffer";
/**
*
@@ -34,6 +35,7 @@ export class SmartWeaveGlobal {
gasLimit: number;
transaction: Transaction;
block: Block;
vrf: Vrf;
arweave: Pick<Arweave, 'ar' | 'wallets' | 'utils' | 'crypto'>;
contract: {
id: string;
@@ -102,6 +104,7 @@ export class SmartWeaveGlobal {
throw new Error('Not implemented - should be set by HandlerApi implementor');
}
};
this.vrf = new Vrf(this);
this.useGas = this.useGas.bind(this);
}
@@ -189,3 +192,30 @@ class Block {
return this.global._activeTx.block.timestamp;
}
}
class Vrf {
constructor(private readonly global: SmartWeaveGlobal) {}
get data(): VrfData {
return this.global._activeTx.vrf;
}
// returns the original generated random number as a BigInt string;
get value(): string {
return this.global._activeTx.vrf.bigint;
}
// returns a random value in a range from 1 to maxValue
randomInt(maxValue: number): number {
if (!Number.isInteger(maxValue)) {
throw new Error('Integer max value required for random integer generation');
}
const result = BigInt(this.global._activeTx.vrf.bigint) % BigInt(maxValue) + BigInt(1);
if (result > Number.MAX_SAFE_INTEGER || result < Number.MIN_SAFE_INTEGER) {
throw new Error('Random int cannot be cast to number');
}
return Number(result);
}
}

1467
yarn.lock

File diff suppressed because it is too large Load Diff