Skip to content

Commit

Permalink
feat(cu): revert dryRun changes permaweb#674
Browse files Browse the repository at this point in the history
This reverts commit 08f0cc5.
  • Loading branch information
TillaTheHun0 committed May 6, 2024
1 parent 6fca341 commit 683297a
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 84 deletions.
203 changes: 125 additions & 78 deletions servers/cu/src/domain/api/dryRun.js
Original file line number Diff line number Diff line change
@@ -1,115 +1,162 @@
import { Readable } from 'node:stream'
import { omit } from 'ramda'
import { of } from 'hyper-async'
import { Resolved, of } from 'hyper-async'

import { loadProcessWith } from '../lib/loadProcess.js'
import { loadModuleWith } from '../lib/loadModule.js'
import { loadMessageMetaWith } from '../lib/loadMessageMeta.js'
import { evaluateWith } from '../lib/evaluate.js'
import { messageSchema } from '../model.js'
import { loadModuleWith } from '../lib/loadModule.js'
import { mapFrom } from '../utils.js'
import { readStateWith } from './readState.js'

/**
* @typedef Env
*
* @typedef Result
*
* @typedef DryRunArgs
* @property {string} processId
* @property {any} dryRun
* @typedef ReadResultArgs
* @property {string} messageTxId
*
* @callback DryRun
* @param {DryRunArgs} args
* @callback ReadResult
* @param {ReadResultArgs} args
* @returns {Promise<Result>} result
*
* @param {Env} - the environment
* @returns {DryRun}
* @returns {ReadResult}
*/
export function dryRunWith (env) {
const loadProcess = loadProcessWith(env)
const loadMessageMeta = loadMessageMetaWith(env)
const loadModule = loadModuleWith(env)
const readState = readStateWith(env)
const evaluate = evaluateWith(env)

return ({ processId, dryRun }) => {
const stats = {
startTime: new Date(),
endTime: undefined,
messages: {
scheduled: 0,
cron: 0
}
}

const logStats = (res) => {
stats.endTime = new Date()
env.logger(
'dryRun for process "%s" at nonce "%s" took %d milliseconds: %j',
processId,
res.ordinate,
stats.endTime - stats.startTime,
stats
)

return res
}
return ({ processId, messageTxId, dryRun }) => {
return of({ messageTxId })
.chain(({ messageTxId }) => {
/**
* Load the metadata associated with the messageId ie.
* it's timestamp and ordinate, so readState can evaluate
* up to that point (if it hasn't already)
*/
if (messageTxId) return loadMessageMeta({ processId, messageTxId })

return of({ id: processId, stats })
.chain(loadProcess)
.chain(loadModule)
.chain((ctx) => {
async function * dryRunMessage () {
/**
* No messageTxId provided so evaluate up to latest
*/
return Resolved({
processId,
to: undefined,
ordinate: undefined
})
})
/**
* Read up to the specified 'to', or latest
*/
.chain((res) =>
readState({
processId: res.processId,
to: res.timestamp,
/**
* Dry run messages are not signed, and therefore
* will not have a verifiable Id, Signature, Owner, etc.
*
* NOTE:
* Dry Run messages are not signed, therefore not verifiable.
* The ordinate for a scheduled message is it's nonce
*/
ordinate: res.nonce && `${res.nonce}`,
/**
* We know this is a scheduled message, and so has no
* associated cron.
*
* This is generally okay, because dry-run message evaluations
* are Read-Only and not persisted -- the primary use-case for Dry Run is to enable
* retrieving a view of a processes state, without having to send a bonafide message.
* So we explicitly set cron to undefined, for posterity
*/
cron: undefined,
/**
* If we are evaluating up to a specific message, as indicated by
* the presence of messageTxId, then we make sure we get an exact match.
*
* However, we should keep in mind the implications. One implication is that spoofing
* Owner or other fields on a Dry-Run message (unverifiable context) exposes a way to
* "poke and prod" a process modules for vulnerabilities.
* Otherwise, we are evaluating up to the latest
*/
yield messageSchema.parse({
exact: !!messageTxId,
needsMemory: true
})
)
/**
* We've read up to 'to', now inject the dry-run message
*
* { id, owner, tags, output: { Memory, Error, Messages, Spawns, Output } }
*/
.chain((readStateRes) => {
return of(readStateRes)
.chain((ctx) => {
/**
* Don't save the dryRun message
* If a cached evaluation was found and immediately returned,
* then we will have not loaded the module and attached it to ctx.
*
* So we check if ctx.module is set, and load the Module if not.
*
* This check will prevent us from unnecessarily loading the module
* from Arweave, twice.
*/
noSave: true,
deepHash: undefined,
cron: undefined,
ordinate: ctx.ordinate,
name: 'Dry Run Message',
message: {
if (!ctx.moduleId) return loadModule(ctx)

/**
* The module was loaded by readState, as part of evaluation,
* so no need to load it again. Just reuse it
*/
return Resolved(ctx)
})
.chain((ctx) => {
async function * dryRunMessage () {
/**
* We default timestamp and block-height using
* the current evaluation.
* Dry run messages are not signed, and therefore
* will not have a verifiable Id, Signature, Owner, etc.
*
* NOTE:
* Dry Run messages are not signed, therefore not verifiable.
*
* The Dry-Run message can overwrite them
* This is generally okay, because dry-run message evaluations
* are Read-Only and not persisted -- the primary use-case for Dry Run is to enable
* retrieving a view of a processes state, without having to send a bonafide message.
*
* However, we should keep in mind the implications. One implication is that spoofing
* Owner or other fields on a Dry-Run message (unverifiable context) exposes a way to
* "poke and prod" a process modules for vulnerabilities.
*/
Timestamp: ctx.from,
'Block-Height': ctx.fromBlockHeight,
Cron: false,
Target: processId,
...dryRun,
From: mapFrom({ tags: dryRun.Tags, owner: dryRun.Owner }),
'Read-Only': true
},
AoGlobal: {
Process: { Id: processId, Owner: ctx.owner, Tags: ctx.tags },
Module: { Id: ctx.moduleId, Owner: ctx.moduleOwner, Tags: ctx.moduleTags }
yield messageSchema.parse({
/**
* Don't save the dryRun message
*/
noSave: true,
deepHash: undefined,
cron: undefined,
ordinate: ctx.ordinate,
name: 'Dry Run Message',
message: {
/**
* We default timestamp and block-height using
* the current evaluation.
*
* The Dry-Run message can overwrite them
*/
Timestamp: ctx.from,
'Block-Height': ctx.fromBlockHeight,
Cron: false,
Target: processId,
...dryRun,
From: mapFrom({ tags: dryRun.Tags, owner: dryRun.Owner }),
'Read-Only': true
},
AoGlobal: {
Process: { Id: processId, Owner: ctx.owner, Tags: ctx.tags },
Module: { Id: ctx.moduleId, Owner: ctx.moduleOwner, Tags: ctx.moduleTags }
}
})
}
})
}

/**
* Pass a messages stream to evaluate that only emits the single dry-run
* message and then completes
*/
return evaluate({ ...ctx, messages: Readable.from(dryRunMessage()) })
/**
* Pass a messages stream to evaluate that only emits the single dry-run
* message and then completes
*/
return evaluate({ ...ctx, messages: Readable.from(dryRunMessage()) })
})
})
.bimap(logStats, logStats)
.map((res) => res.output)
.map(omit(['Memory']))
}
Expand Down
19 changes: 13 additions & 6 deletions servers/cu/src/routes/dryRun.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { always, compose } from 'ramda'
import { z } from 'zod'

import { busyIn } from '../domain/utils.js'
import { withMiddleware, withProcessRestrictionFromQuery } from './middleware/index.js'

const inputSchema = z.object({
processId: z.string().min(1, 'a process-id query parameter is required'),
messageTxId: z.string().min(1, 'to must be a transaction id').optional(),
dryRun: z.object({
Id: z.string().nullish(),
Signature: z.string().nullish(),
Expand All @@ -29,16 +31,21 @@ export const withDryRunRoutes = app => {
withProcessRestrictionFromQuery,
always(async (req, res) => {
const {
query: { 'process-id': processId },
query: { 'process-id': processId, to: messageTxId },
body,
domain: { apis: { dryRun } }
domain: { BUSY_THRESHOLD, apis: { dryRun } }
} = req

const input = inputSchema.parse({ processId, dryRun: body })
const input = inputSchema.parse({ processId, messageTxId, dryRun: body })

await dryRun(input)
.map((output) => res.send(output))
.toPromise()
await busyIn(
BUSY_THRESHOLD,
dryRun(input).toPromise(),
() => {
res.status(202)
return { message: `Evaluation of process "${input.processId}" to "${input.messageTxId || 'latest'}" is in progress.` }
}
).then((output) => res.send(output))
})
)()
)
Expand Down

0 comments on commit 683297a

Please sign in to comment.