diff --git a/src/__tests__/unit/evaluation-options.test.ts b/src/__tests__/unit/evaluation-options.test.ts index d882477c..90fed147 100644 --- a/src/__tests__/unit/evaluation-options.test.ts +++ b/src/__tests__/unit/evaluation-options.test.ts @@ -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/', @@ -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/', @@ -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/', @@ -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/', @@ -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/', @@ -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/', diff --git a/src/contract/EvaluationOptionsEvaluator.ts b/src/contract/EvaluationOptionsEvaluator.ts index 21a6165a..a11eec9c 100644 --- a/src/contract/EvaluationOptionsEvaluator.ts +++ b/src/contract/EvaluationOptionsEvaluator.ts @@ -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']; } @@ -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' ]; /** diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 9db2c6c0..1bf1aee7 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -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'; @@ -689,7 +694,6 @@ export class HandlerBasedContract implements Contract { 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('vrf'); if (vrfPlugin) { dummyTx.vrf = vrfPlugin.process().generateMockVrf(dummyTx.sortKey); @@ -954,25 +958,67 @@ export class HandlerBasedContract implements Contract { 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(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({ diff --git a/src/core/KnownTags.ts b/src/core/KnownTags.ts index d9809630..8bdd8fa3 100644 --- a/src/core/KnownTags.ts +++ b/src/core/KnownTags.ts @@ -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', diff --git a/src/core/modules/StateEvaluator.ts b/src/core/modules/StateEvaluator.ts index da1e652e..a9907ec5 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -103,6 +103,13 @@ export class EvalStateResult { 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 @@ -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. @@ -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; } diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index bef26920..2c4891b3 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -14,6 +14,7 @@ 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; @@ -245,22 +246,34 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { } if (internalWrites && contract.isRoot() && result.type === 'ok') { - 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; + 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 { - 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; + 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'; diff --git a/src/core/modules/impl/TagsParser.ts b/src/core/modules/impl/TagsParser.ts index 753acb14..c6469d1b 100644 --- a/src/core/modules/impl/TagsParser.ts +++ b/src/core/modules/impl/TagsParser.ts @@ -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. @@ -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 { const result = new Map(); @@ -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; + } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index bfa24e65..ef64c059 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -92,7 +92,7 @@ export async function getJsonResponse(response: Promise): Promise