Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: iw validation #428

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/__tests__/unit/evaluation-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('Evaluation options evaluator', () => {
maxCallDepth: 7,
maxInteractionEvaluationTimeSeconds: 60,
mineArLocalBlocks: true,
remoteInternalWrite: false,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
Expand Down Expand Up @@ -54,6 +55,7 @@ describe('Evaluation options evaluator', () => {
maxCallDepth: 7,
maxInteractionEvaluationTimeSeconds: 60,
mineArLocalBlocks: true,
remoteInternalWrite: false,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
Expand Down Expand Up @@ -86,6 +88,7 @@ describe('Evaluation options evaluator', () => {
maxCallDepth: 5,
maxInteractionEvaluationTimeSeconds: 60,
mineArLocalBlocks: true,
remoteInternalWrite: false,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
Expand Down Expand Up @@ -115,6 +118,7 @@ describe('Evaluation options evaluator', () => {
maxCallDepth: 5,
maxInteractionEvaluationTimeSeconds: 60,
mineArLocalBlocks: true,
remoteInternalWrite: false,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
Expand Down Expand Up @@ -144,6 +148,7 @@ describe('Evaluation options evaluator', () => {
maxCallDepth: 5,
maxInteractionEvaluationTimeSeconds: 60,
mineArLocalBlocks: true,
remoteInternalWrite: false,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
Expand Down Expand Up @@ -173,6 +178,7 @@ describe('Evaluation options evaluator', () => {
maxCallDepth: 5,
maxInteractionEvaluationTimeSeconds: 60,
mineArLocalBlocks: true,
remoteInternalWrite: false,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
Expand Down
8 changes: 5 additions & 3 deletions src/contract/EvaluationOptionsEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class EvaluationOptionsEvaluator {

if (this.rootOptions['unsafeClient'] === 'allow') {
if (foreignOptions['unsafeClient'] === 'throw') {
return 'skip'; // we don't the foreing contract to stop the evaluation of the root contract
return 'skip'; // we don't want the foreign contract to stop the evaluation of the root contract
} else {
return foreignOptions['unsafeClient'];
}
Expand Down Expand Up @@ -106,13 +106,15 @@ export class EvaluationOptionsEvaluator {
remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'],
remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'],
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'],
useConstructor: (foreignOptions) => foreignOptions['useConstructor']
useConstructor: (foreignOptions) => foreignOptions['useConstructor'],
remoteInternalWrite: (foreignOptions) => foreignOptions['remoteInternalWrite']
};

private readonly notConflictingEvaluationOptions: (keyof EvaluationOptions)[] = [
'useKVStorage',
'sourceType',
'useConstructor'
'useConstructor',
'remoteInternalWrite'
];

/**
Expand Down
84 changes: 65 additions & 19 deletions src/contract/HandlerBasedContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
} from '../core/modules/impl/HandlerExecutorFactory';
import { LexicographicalInteractionsSorter } from '../core/modules/impl/LexicographicalInteractionsSorter';
import { InteractionsSorter } from '../core/modules/InteractionsSorter';
import { DefaultEvaluationOptions, EvalStateResult, EvaluationOptions } from '../core/modules/StateEvaluator';
import {
DefaultEvaluationOptions,
EvalStateResult,
EvaluationOptions,
InternalWriteEvalResult
} from '../core/modules/StateEvaluator';
import { WARP_TAGS } from '../core/KnownTags';
import { Warp } from '../core/Warp';
import { createDummyTx, createInteractionTx } from '../legacy/create-interaction-tx';
Expand Down Expand Up @@ -689,7 +694,6 @@ 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) {
Arweave.utils;
const vrfPlugin = this.warp.maybeLoadPlugin<void, VrfPluginFunctions>('vrf');
if (vrfPlugin) {
dummyTx.vrf = vrfPlugin.process().generateMockVrf(dummyTx.sortKey);
Expand Down Expand Up @@ -954,25 +958,67 @@ export class HandlerBasedContract<State> implements Contract<State> {
strict: boolean,
vrf: boolean
) {
const handlerResult = await this.callContract(
input,
'write',
undefined,
undefined,
tags,
transfer,
strict,
vrf,
false
);
const innerWrites = [];

if (this.evaluationOptions().remoteInternalWrite) {
// there's probably a less dumb way of doin' this.
const baseDreUrl = this.evaluationOptions().remoteStateSyncSource.split('/')[1];

const walletAddress = await this._signature.getAddress();
const iwEvalUrl = `https://${baseDreUrl}/internal-write?contractTxId=${this.txId()}&caller=${walletAddress}&vrf=${vrf}&strict=${strict}&input=${JSON.stringify(
input
)}`;
const result = await getJsonResponse<InternalWriteEvalResult>(fetch(iwEvalUrl));
if (result.errorMessage) {
throw new Error(`Error while generating internal writes, cause: ${result.errorMessage}`);
}
const stringifiedContracts = stringify(result.contracts);

if (strict && handlerResult.type !== 'ok') {
throw Error('Cannot create interaction: ' + JSON.stringify(handlerResult.error || handlerResult.errorMessage));
// TODO: add trusted nodes jwk.n in EvaluationOptions
const verified = await Arweave.crypto.verify(
result.publicModulus,
Arweave.utils.stringToBuffer(stringifiedContracts),
Arweave.utils.b64UrlToBuffer(result.signature)
);
if (!verified) {
throw new Error('Could not verify the internal writes response from DRE');
}
innerWrites.push(...result.contracts);
tags.push(
{
name: WARP_TAGS.INTERACT_WRITE_SIG,
value: result.signature
},
{
name: WARP_TAGS.INTERACT_WRITE_SIGNER,
value: result.publicModulus
},
{
name: WARP_TAGS.INTERACT_WRITE_SIG_DATA,
value: stringifiedContracts
}
);
} else {
const handlerResult = await this.callContract(
input,
'write',
undefined,
undefined,
tags,
transfer,
strict,
vrf,
false
);

if (strict && handlerResult.type !== 'ok') {
throw Error('Cannot create interaction: ' + JSON.stringify(handlerResult.error || handlerResult.errorMessage));
}
const callStack: ContractCallRecord = this.getCallStack();
innerWrites.push(...this._innerWritesEvaluator.eval(callStack));
this.logger.debug('Input', input);
this.logger.debug('Callstack', callStack.print());
}
const callStack: ContractCallRecord = this.getCallStack();
const innerWrites = this._innerWritesEvaluator.eval(callStack);
this.logger.debug('Input', input);
this.logger.debug('Callstack', callStack.print());

innerWrites.forEach((contractTxId) => {
tags.push({
Expand Down
9 changes: 7 additions & 2 deletions src/contract/InnerWritesEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ export class InnerWritesEvaluator {
return result;
}

private evalForeignCalls(rootContractTxId: string, interaction: InteractionCall, result: Array<string>) {
evalForeignCalls(
rootContractTxId: string,
interaction: InteractionCall,
result: Array<string>,
onlyDryWrites = true
) {
Object.keys(interaction.interactionInput.foreignContractCalls).forEach((foreignContractCallKey) => {
const foreignContractCall = interaction.interactionInput.foreignContractCalls[foreignContractCallKey];
if (foreignContractCall.innerCallType == 'write') {
Object.keys(foreignContractCall.interactions).forEach((k) => {
const foreignInteraction = foreignContractCall.interactions[k];
if (
foreignInteraction.interactionInput.dryWrite &&
((onlyDryWrites && foreignInteraction.interactionInput.dryWrite) || !onlyDryWrites) &&
!result.includes(foreignContractCall.contractTxId) &&
rootContractTxId !== foreignContractCall.contractTxId /*"write-backs"*/
) {
Expand Down
3 changes: 3 additions & 0 deletions src/core/KnownTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const WARP_TAGS = {
INIT_STATE: 'Init-State',
INIT_STATE_TX: 'Init-State-TX',
INTERACT_WRITE: 'Interact-Write',
INTERACT_WRITE_SIG: 'Interact-Write-Sig',
INTERACT_WRITE_SIGNER: 'Interact-Write-Signer',
INTERACT_WRITE_SIG_DATA: 'Interact-Write-Sig-Data',
WASM_LANG: 'Wasm-Lang',
WASM_LANG_VERSION: 'Wasm-Lang-Version',
WASM_META: 'Wasm-Meta',
Expand Down
13 changes: 13 additions & 0 deletions src/core/modules/StateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ export class EvalStateResult<State> {

export type UnsafeClientOptions = 'allow' | 'skip' | 'throw';

export type InternalWriteEvalResult = {
contracts: string[];
signature: string;
publicModulus: string;
errorMessage: string;
};

export class DefaultEvaluationOptions implements EvaluationOptions {
// default = true - still cannot decide whether true or false should be the default.
// "false" may lead to some fairly simple attacks on contract, if the contract
Expand Down Expand Up @@ -150,6 +157,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
remoteStateSyncSource = 'https://dre-1.warp.cc/contract';

useConstructor = false;

remoteInternalWrite = false;
}

// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
Expand Down Expand Up @@ -238,4 +247,8 @@ export interface EvaluationOptions {

// remote source for fetching most recent contract state, only applicable if remoteStateSyncEnabled is set to true
remoteStateSyncSource: string;

// whether the internal writes discovery should evaluate locally - or by the trusted D.R.E. node.
// if set to 'true', the D.R.E. from the 'remoteStateSyncSource' will be used.
remoteInternalWrite: boolean;
}
50 changes: 42 additions & 8 deletions src/core/modules/impl/DefaultStateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExe
import { TagsParser } from './TagsParser';
import { VrfPluginFunctions } from '../../WarpPlugin';
import { BasicSortKeyCache } from '../../../cache/BasicSortKeyCache';
import { InnerWritesEvaluator } from '../../../contract/InnerWritesEvaluator';
import stringify from 'safe-stable-stringify';

type EvaluationProgressInput = {
contractTxId: string;
Expand Down Expand Up @@ -239,22 +241,54 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {

this.logger.debug(`${indent(depth)}Interaction evaluation`, singleInteractionBenchmark.elapsed());

if (result.type === 'exception' && ignoreExceptions !== true) {
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.errorMessage}`);
}

if (internalWrites && contract.isRoot() && result.type === 'ok') {
const iwSigData = this.tagsParser.getInternalWritesSigTags(missingInteraction);
if (iwSigData) {
const verified = await Arweave.crypto.verify(
iwSigData.publicModulus,
Arweave.utils.stringToBuffer(stringify(iwSigData.contracts)),
Arweave.utils.b64UrlToBuffer(iwSigData.signature)
);
if (!verified) {
throw new Error('Could not verify the internal writes response from DRE');
}
} else {
const innerWritesEvaluator = new InnerWritesEvaluator();
const iwEvaluatorResult = [];
innerWritesEvaluator.evalForeignCalls(contract.txId(), interactionCall, iwEvaluatorResult, false);
const tagsInnerWrites = this.tagsParser.getInteractWritesContracts(missingInteraction);
if (
iwEvaluatorResult.length == tagsInnerWrites.length &&
tagsInnerWrites.every((elem) => iwEvaluatorResult.includes(elem))
) {
validity[missingInteraction.id] = result.type === 'ok';
currentState = result.state;
} else {
validity[missingInteraction.id] = false;
errorMessage = `[SDK] Inner writes do not match - tags: ${tagsInnerWrites}, evaluated: ${iwEvaluatorResult}`;
// console.error(errorMessage);
// console.dir(interactionCall, { depth: null });
errorMessages[missingInteraction.id] = errorMessage;
}
}
} else {
validity[missingInteraction.id] = result.type === 'ok';
currentState = result.state;
}

interactionCall.update({
cacheHit: false,
outputState: stackTrace.saveState ? currentState : undefined,
executionTime: singleInteractionBenchmark.elapsed(true) as number,
valid: validity[missingInteraction.id],
errorMessage: errorMessage,
errorMessage,
gasUsed: result.gasUsed
});

if (result.type === 'exception' && ignoreExceptions !== true) {
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.errorMessage}`);
}

validity[missingInteraction.id] = result.type === 'ok';
currentState = result.state;

const toCache = new EvalStateResult(currentState, validity, errorMessages);
if (canBeCached(missingInteraction)) {
lastConfirmedTxState = {
Expand Down
21 changes: 21 additions & 0 deletions src/core/modules/impl/TagsParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SMART_WEAVE_TAGS, WARP_TAGS } from '../../KnownTags';
import { GQLNodeInterface, GQLTagInterface } from '../../../legacy/gqlResult';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { Transaction } from '../../../utils/types/arweave-types';
import { InternalWriteEvalResult } from '../StateEvaluator';

/**
* A class that is responsible for retrieving "input" tag from the interaction transaction.
Expand Down Expand Up @@ -58,6 +59,22 @@ export class TagsParser {
return interactionTransaction.tags.find((tag) => tag.name === SMART_WEAVE_TAGS.CONTRACT_TX_ID)?.value;
}

getInternalWritesSigTags(interactionTransaction: GQLNodeInterface): InternalWriteEvalResult | null {
const iwSigTag = this.findTag(interactionTransaction, WARP_TAGS.INTERACT_WRITE_SIG);
if (iwSigTag) {
const iwSignerTag = this.findTag(interactionTransaction, WARP_TAGS.INTERACT_WRITE_SIGNER);
const iwSigDataTag = this.findTag(interactionTransaction, WARP_TAGS.INTERACT_WRITE_SIG_DATA);
return {
contracts: JSON.parse(iwSigDataTag),
signature: iwSigTag,
publicModulus: iwSignerTag,
errorMessage: null
};
} else {
return null;
}
}

getContractsWithInputs(interactionTransaction: GQLNodeInterface): Map<string, GQLTagInterface> {
const result = new Map<string, GQLTagInterface>();

Expand Down Expand Up @@ -118,4 +135,8 @@ export class TagsParser {
return t.name == WARP_TAGS.REQUEST_VRF && t.value === 'true';
});
}

private findTag(interactionTransaction: GQLNodeInterface, tagName: string): string | undefined {
return interactionTransaction.tags.find((tag) => tag.name === tagName)?.value;
}
}
2 changes: 1 addition & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function getJsonResponse<T>(response: Promise<Response>): Promise<T
try {
r = await response;
} catch (e) {
throw new Error(`Error while communicating with gateway: ${JSON.stringify(e)}`);
throw new Error(`Error while communicating with server: ${JSON.stringify(e)}`);
}

if (!r?.ok) {
Expand Down