Skip to content
122 changes: 94 additions & 28 deletions src/localFunctionsTestnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
callReportGasLimit,
simulatedSecretsKeys,
simulatedTransmitters,
numberOfSimulatedNodeExecutions,
} from './simulationConfig'
import {
LinkTokenSource,
Expand All @@ -23,9 +24,9 @@ import {
FunctionsClientExampleSource,
} from './v1_contract_sources'

import type { Server, Ethereum } from 'ganache'
import type { Server, ServerOptions } from 'ganache'

import type { FunctionsRequestParams, RequestCommitment } from './types'
import type { FunctionsRequestParams, RequestCommitment, SimulationResult } from './types'

export interface RequestEventData {
requestId: string
Expand All @@ -49,6 +50,8 @@ interface FunctionsContracts {

export const startLocalFunctionsTestnet = async (
port = 8545,
secrets?: Record<string, string>,
options?: ServerOptions,
): Promise<
{
server: Server
Expand All @@ -63,13 +66,7 @@ export const startLocalFunctionsTestnet = async (
close: () => Promise<void>
} & FunctionsContracts
> => {
const server = Ganache.server({
logging: {
debug: false,
verbose: false,
quiet: true,
},
})
const server = Ganache.server(options)

server.listen(port, 'localhost', (err: Error | null) => {
if (err) {
Expand Down Expand Up @@ -113,7 +110,7 @@ export const startLocalFunctionsTestnet = async (
callbackGasLimit,
commitment,
}
handleOracleRequest(requestEvent, contracts.mockCoordinator, admin)
handleOracleRequest(requestEvent, contracts.mockCoordinator, admin, secrets)
},
)

Expand Down Expand Up @@ -155,14 +152,11 @@ const handleOracleRequest = async (
requestEventData: RequestEventData,
mockCoordinator: Contract,
admin: Wallet,
secrets: Record<string, string> = {},
) => {
const requestData = await constructRequestDataObject(requestEventData.data)
const response = await simulateScript({
source: requestData.source,
secrets: {}, // TODO: Decrypt secrets
args: requestData.args,
// TODO: Support bytes args
})
const response = await simulateDONExecution(requestData, secrets)

const errorHexstring = response.errorString
? '0x' + Buffer.from(response.errorString.toString()).toString('hex')
: undefined
Expand All @@ -183,6 +177,76 @@ const handleOracleRequest = async (
await reportTx.wait(1)
}

const simulateDONExecution = async (
requestData: FunctionsRequestParams,
secrets: Record<string, string>,
): Promise<{ responseBytesHexstring?: string; errorString?: string }> => {
// Perform the simulation numberOfSimulatedNodeExecution times
const simulations = [...Array(numberOfSimulatedNodeExecutions)].map(() =>
simulateScript({
source: requestData.source,
secrets,
args: requestData.args,
bytesArgs: requestData.bytesArgs,
}),
)
const responses = await Promise.all(simulations)

const successfulResponses = responses.filter(response => response.errorString === undefined)
const errorResponses = responses.filter(response => response.errorString !== undefined)

if (successfulResponses.length > errorResponses.length) {
return {
responseBytesHexstring: aggregateMedian(
successfulResponses.map(response => response.responseBytesHexstring!),
),
}
} else {
return {
errorString: aggregateModeString(errorResponses.map(response => response.errorString!)),
}
}
}

const aggregateMedian = (responses: string[]): string => {
const bufResponses = responses.map(response => Buffer.from(response.slice(2), 'hex'))

bufResponses.sort((a, b) => {
if (a.length !== b.length) {
return a.length - b.length
}
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) {
return a[i] - b[i]
}
}
return 0
})

return '0x' + bufResponses[Math.floor((bufResponses.length - 1) / 2)].toString('hex')
}

const aggregateModeString = (items: string[]): string => {
const counts = new Map<string, number>()

for (const str of items) {
const existingCount = counts.get(str) || 0
counts.set(str, existingCount + 1)
}

let modeString = items[0]
let maxCount = counts.get(modeString) || 0

for (const [str, count] of counts.entries()) {
if (count > maxCount) {
maxCount = count
modeString = str
}
}

return modeString
}

const encodeReport = (
requestId: string,
commitment: RequestCommitment,
Expand Down Expand Up @@ -226,35 +290,37 @@ const encodeReport = (

const constructRequestDataObject = async (requestData: string): Promise<FunctionsRequestParams> => {
const decodedRequestData = await cbor.decodeAll(Buffer.from(requestData.slice(2), 'hex'))

const requestDataObject = {} as FunctionsRequestParams

for (let i = 0; i < decodedRequestData.length - 1; i += 2) {
const elem = decodedRequestData[i]
// TODO: support encrypted secrets & bytesArgs
switch (elem) {
case 'codeLocation':
requestDataObject.codeLocation = decodedRequestData[i + 1]
break
// case 'secretsLocation':
// requestDataObject.secretsLocation = decodedRequestData[i + 1]
// break
case 'secretsLocation':
// Unused as secrets provided as an argument to startLocalFunctionsTestnet() are used instead
break
case 'language':
requestDataObject.codeLanguage = decodedRequestData[i + 1]
break
case 'source':
requestDataObject.source = decodedRequestData[i + 1]
break
// case 'encryptedSecretsReference':
// requestDataObject.encryptedSecretsReference = decodedRequestData[i + 1]
// break
case 'secrets':
// Unused as secrets provided as an argument to startLocalFunctionsTestnet() are used instead
break
case 'args':
requestDataObject.args = decodedRequestData[i + 1]
break
// case 'bytesArgs':
// requestDataObject.bytesArgs = decodedRequestData[i + 1]
// break
// default:
// throw Error(`Invalid request data key ${elem}`)
case 'bytesArgs':
requestDataObject.bytesArgs = decodedRequestData[i + 1].map((bytesArg: Buffer) => {
return '0x' + bytesArg.toString('hex')
})
break
default:
// Ignore unknown keys
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/simulateScript/simulateScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ export const simulateScript = async ({

// Check if deno is installed
try {
const result = execSync('deno --version', { stdio: 'pipe' })
console.log(`Performing simulation with the following versions:\n${result.toString()}\n`)
execSync('deno --version', { stdio: 'pipe' })
} catch {
throw Error(
'Deno must be installed and accessible via the PATH environment variable (ie: the `deno --version` command must work).\nVisit https://deno.land/#installation for installation instructions.',
Expand Down
2 changes: 2 additions & 0 deletions src/simulationConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const simulatedAllowListConfig = {

export const callReportGasLimit = 5_000_000

export const numberOfSimulatedNodeExecutions = 4

export const simulatedWallets = {
node0: {
address: '0xAe24F6e7e046a0C764DF51F333dE5e2fE360AC72',
Expand Down
10 changes: 8 additions & 2 deletions test/integration/ResponseListener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ describe('Functions toolkit classes', () => {
let allowlistedUser_A: Wallet

beforeAll(async () => {
const port = 9501
const localFunctionsTestnet = await startLocalFunctionsTestnet(port)
const port = 8002
const localFunctionsTestnet = await startLocalFunctionsTestnet(port, undefined, {
logging: {
debug: false,
verbose: false,
quiet: true,
},
})

linkTokenAddress = localFunctionsTestnet.linkToken.address
functionsRouterAddress = localFunctionsTestnet.router.address
Expand Down
14 changes: 12 additions & 2 deletions test/integration/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ describe('Functions toolkit classes', () => {
let subFunder_A: Wallet

beforeAll(async () => {
const localFunctionsTestnet = await startLocalFunctionsTestnet()
const port = 8001
const localFunctionsTestnet = await startLocalFunctionsTestnet(port, undefined, {
logging: {
debug: false,
verbose: false,
quiet: true,
},
})

linkTokenContract = localFunctionsTestnet.linkToken
linkTokenAddress = localFunctionsTestnet.linkToken.address
Expand All @@ -38,7 +45,10 @@ describe('Functions toolkit classes', () => {
consumerAddress = localFunctionsTestnet.exampleClient.address
close = localFunctionsTestnet.close

const [admin, walletA, walletB, walletC, _] = createTestWallets(localFunctionsTestnet.server)
const [admin, walletA, walletB, walletC, _] = createTestWallets(
localFunctionsTestnet.server,
port,
)
allowlistedUser_A = walletA
allowlistedUser_B_NoLINK = walletB
subFunder_A = walletC
Expand Down
Loading