From d3341e81286d368a20948051d8f641bbb1c1105b Mon Sep 17 00:00:00 2001 From: Asia Date: Wed, 2 Aug 2023 17:39:36 +0200 Subject: [PATCH] feat: whitelist contract sources (#449) * feat: whitelist contract sources * feat: sort interactions loaded from Warp Gateway --- .../basic/whitelist-sources-evolve.test.ts | 119 ++++++++++++ .../whitelist-sources-foreign-read.test.ts | 172 ++++++++++++++++++ .../whitelist-sources-foreign-write.test.ts | 167 +++++++++++++++++ .../basic/whitelist-sources.test.ts | 94 ++++++++++ .../integration/data/token-pst-foreign.js | 73 ++++++++ src/__tests__/unit/evaluation-options.test.ts | 18 +- .../unit/gateway-interactions.loader.test.ts | 84 ++++----- src/contract/EvaluationOptionsEvaluator.ts | 3 +- src/core/modules/StateEvaluator.ts | 4 + .../modules/impl/DefaultStateEvaluator.ts | 11 +- .../modules/impl/HandlerExecutorFactory.ts | 51 ++++-- .../impl/handler/AbstractContractHandler.ts | 7 +- src/core/modules/impl/handler/JsHandlerApi.ts | 10 +- .../modules/impl/handler/WasmHandlerApi.ts | 10 +- src/plugins/Evolve.ts | 6 +- 15 files changed, 753 insertions(+), 76 deletions(-) create mode 100644 src/__tests__/integration/basic/whitelist-sources-evolve.test.ts create mode 100644 src/__tests__/integration/basic/whitelist-sources-foreign-read.test.ts create mode 100644 src/__tests__/integration/basic/whitelist-sources-foreign-write.test.ts create mode 100644 src/__tests__/integration/basic/whitelist-sources.test.ts create mode 100644 src/__tests__/integration/data/token-pst-foreign.js diff --git a/src/__tests__/integration/basic/whitelist-sources-evolve.test.ts b/src/__tests__/integration/basic/whitelist-sources-evolve.test.ts new file mode 100644 index 0000000..67dd653 --- /dev/null +++ b/src/__tests__/integration/basic/whitelist-sources-evolve.test.ts @@ -0,0 +1,119 @@ +import fs from 'fs'; + +import ArLocal from 'arlocal'; +import Arweave from 'arweave'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import path from 'path'; +import { PstState, PstContract } from '../../../contract/PstContract'; +import { Warp } from '../../../core/Warp'; +import { WarpFactory } from '../../../core/WarpFactory'; +import { LoggerFactory } from '../../../logging/LoggerFactory'; +import { DeployPlugin } from 'warp-contracts-plugin-deploy'; + +describe('Testing sources whitelisting in nested contracts (evolve)', () => { + let contractSrc: string; + + let wallet: JWKInterface; + let walletAddress: string; + + let initialState: PstState; + + let arweave: Arweave; + let arlocal: ArLocal; + let warp: Warp; + let pst: PstContract; + let contractTxId: string; + let srcTxId: string; + + beforeAll(async () => { + arlocal = new ArLocal(1903, false); + await arlocal.start(); + LoggerFactory.INST.logLevel('error'); + warp = WarpFactory.forLocal(1903).use(new DeployPlugin()); + + ({ arweave } = warp); + ({ jwk: wallet, address: walletAddress } = await warp.generateWallet()); + + 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 + } + } + }; + + ({ contractTxId, srcTxId } = await warp.deploy({ + wallet, + initState: JSON.stringify(initialState), + src: contractSrc + })); + pst = warp.pst(contractTxId).setEvaluationOptions({ + whitelistSources: [srcTxId] + }) as PstContract; + pst.connect(wallet); + }); + + afterAll(async () => { + await arlocal.stop(); + }); + + it('should read pst state and balance data', async () => { + expect(await pst.currentState()).toEqual(initialState); + + expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000); + expect((await pst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).balance).toEqual(23111222); + expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669); + }); + + it('should properly transfer tokens', async () => { + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555); + expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555); + }); + + it('should stop evaluation after evolve to non-whitelisted source', async () => { + expect((await pst.currentState()).balances[walletAddress]).toEqual(555114); + + const srcTx = await warp.createSource({ src: contractSrc }, wallet); + const newSrcTxId = await warp.saveSource(srcTx); + + const evolveResponse = await pst.evolve(newSrcTxId); + + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + // note: should not evolve - the balance should be 555114 (the evolved version ads 555 to the balance) + expect((await pst.currentBalance(walletAddress)).balance).toEqual(555114); + + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + await expect(pst.readState()).rejects.toThrow( + `[NonWhitelistedSourceError] Contract source not part of whitelisted sources list: ${newSrcTxId}.` + ); + + // testcase for new warp instance + const newWarp = WarpFactory.forLocal(1903).use(new DeployPlugin()); + const freshPst = newWarp.contract(contractTxId); + const freshResult = await freshPst.readState(); + // note: should not evaluate at all the last interaction + expect(Object.keys(freshResult.cachedValue.validity).length).toEqual(4); + expect(Object.keys(freshResult.cachedValue.errorMessages).length).toEqual(0); + + expect(freshResult.cachedValue.validity[evolveResponse.originalTxId]).toBe(true); + }); +}); diff --git a/src/__tests__/integration/basic/whitelist-sources-foreign-read.test.ts b/src/__tests__/integration/basic/whitelist-sources-foreign-read.test.ts new file mode 100644 index 0000000..bc03bc2 --- /dev/null +++ b/src/__tests__/integration/basic/whitelist-sources-foreign-read.test.ts @@ -0,0 +1,172 @@ +import fs from 'fs'; + +import ArLocal from 'arlocal'; +import Arweave from 'arweave'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import path from 'path'; +import { PstContract, PstState } from '../../../contract/PstContract'; +import { Warp } from '../../../core/Warp'; +import { WarpFactory } from '../../../core/WarpFactory'; +import { LoggerFactory } from '../../../logging/LoggerFactory'; +import { DeployPlugin } from 'warp-contracts-plugin-deploy'; + +describe('Testing sources whitelisting in nested contracts (read)', () => { + let contractSrc: string; + let foreignSrc: string; + + let wallet: JWKInterface; + let walletAddress: string; + + let initialState: PstState; + + let arweave: Arweave; + let arlocal: ArLocal; + let warp: Warp; + let pst, blacklistPst, whitelistPst: PstContract; + let blacklistSrcTxId: string; + let foreignWhitelistTxId, foreignBlacklistTxId: string; + + beforeAll(async () => { + arlocal = new ArLocal(1901, false); + await arlocal.start(); + LoggerFactory.INST.logLevel('debug'); + warp = WarpFactory.forLocal(1901).use(new DeployPlugin()); + + ({ arweave } = warp); + ({ jwk: wallet, address: walletAddress } = await warp.generateWallet()); + + contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8'); + const mainSrcTx = await warp.createSource({ src: contractSrc }, wallet); + const mainSrcTxId = await warp.saveSource(mainSrcTx); + + foreignSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-foreign.js'), 'utf8'); + const blacklistContractSrcTx = await warp.createSource({ src: foreignSrc }, wallet); + blacklistSrcTxId = await warp.saveSource(blacklistContractSrcTx); + const whitelistSrcTx = await warp.createSource({ src: foreignSrc }, wallet); + const whitelistSrcTxId = await warp.saveSource(whitelistSrcTx); + + 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 warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId: mainSrcTxId + }); + + ({ contractTxId: foreignWhitelistTxId } = await warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId: whitelistSrcTxId + })); + + ({ contractTxId: foreignBlacklistTxId } = await warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId: blacklistSrcTxId + })); + + pst = warp.pst(contractTxId).setEvaluationOptions({ + whitelistSources: [mainSrcTxId, whitelistSrcTxId] + }) as PstContract; + pst.connect(wallet); + + blacklistPst = warp.pst(foreignBlacklistTxId).connect(wallet) as PstContract; + + whitelistPst = warp.pst(foreignWhitelistTxId).connect(wallet) as PstContract; + }); + + afterAll(async () => { + await arlocal.stop(); + }); + + it('should properly transfer tokens', async () => { + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555); + expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555); + }); + + it('should properly read foreign contract with whitelisted source', async () => { + await whitelistPst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + const { originalTxId } = await pst.writeInteraction({ + function: 'readForeign', + contractTxId: foreignWhitelistTxId + }); + + const result = await pst.readState(); + expect(result.cachedValue.validity[originalTxId]).toBe(true); + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1); + }); + + it('should stop evaluation of a contract which is not in the whitelist (readContractState)', async () => { + const readBlacklistedTx = await pst.writeInteraction({ + function: 'readForeign', + contractTxId: foreignBlacklistTxId + }); + + const result = await pst.readState(); + + expect(Object.keys(result.cachedValue.validity).length == 2); + expect(Object.keys(result.cachedValue.errorMessages).length == 2); + + expect(result.cachedValue.validity[readBlacklistedTx.originalTxId]).toBe(false); + expect(result.cachedValue.errorMessages[readBlacklistedTx.originalTxId]).toMatch( + `Contract source not part of whitelisted sources list: ${blacklistSrcTxId}.` + ); + + // should not change from previous test + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1); + }); + + it('should skip evaluation when foreign whitelisted contract evolves to non-whitelisted source (readContractState)', async () => { + const readWhitelistedTx = await pst.writeInteraction({ + function: 'readForeign', + contractTxId: foreignWhitelistTxId + }); + + await whitelistPst.evolve(blacklistSrcTxId); + + await whitelistPst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + const readEvolvedBlacklistTx = await pst.writeInteraction({ + function: 'readForeign', + contractTxId: foreignWhitelistTxId + }); + + const lastWrittenTx = await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + const result = await pst.readState(); + expect(result.cachedValue.validity[readWhitelistedTx.originalTxId]).toBe(true); + expect(result.cachedValue.validity[readEvolvedBlacklistTx.originalTxId]).toBe(false); + + // note: the transactions after foreign read from evolved to unsafe contract should be processed normally + expect(result.cachedValue.validity[lastWrittenTx.originalTxId]).toBe(true); + + // should be incremented by one - only the first read from this testcase should be successful + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(2); + }); +}); diff --git a/src/__tests__/integration/basic/whitelist-sources-foreign-write.test.ts b/src/__tests__/integration/basic/whitelist-sources-foreign-write.test.ts new file mode 100644 index 0000000..fdb8068 --- /dev/null +++ b/src/__tests__/integration/basic/whitelist-sources-foreign-write.test.ts @@ -0,0 +1,167 @@ +import fs from 'fs'; + +import ArLocal from 'arlocal'; +import Arweave from 'arweave'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import path from 'path'; +import { PstContract, PstState } from '../../../contract/PstContract'; +import { Warp } from '../../../core/Warp'; +import { WarpFactory } from '../../../core/WarpFactory'; +import { LoggerFactory } from '../../../logging/LoggerFactory'; +import { DeployPlugin } from 'warp-contracts-plugin-deploy'; + +describe('Testing sources whitelisting in nested contracts (write)', () => { + let contractSrc, foreignContractSrc: string; + + let wallet: JWKInterface; + let walletAddress: string; + + let initialState: PstState; + + let arweave: Arweave; + let arlocal: ArLocal; + let warp, warpBlacklisted: Warp; + let pst, foreignWhitelistedPst, foreignBlacklistedPst: PstContract; + let contractTxId, + foreignBlacklistedContractTxId, + foreignBlacklistedSrcTxId, + foreignWhitelistedContractTxId, + foreignWhitelistedSrcTxId; + + beforeAll(async () => { + arlocal = new ArLocal(1902, false); + await arlocal.start(); + LoggerFactory.INST.logLevel('error'); + warp = WarpFactory.forLocal(1902).use(new DeployPlugin()); + warpBlacklisted = WarpFactory.forLocal(1902).use(new DeployPlugin()); + + ({ arweave } = warp); + ({ jwk: wallet, address: walletAddress } = await warp.generateWallet()); + + contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8'); + const srcTx = await warp.createSource({ src: contractSrc }, wallet); + const srcTxId = await warp.saveSource(srcTx); + + foreignContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-foreign.js'), 'utf8'); + const foreignBlacklistContractSrcTx = await warp.createSource({ src: foreignContractSrc }, wallet); + foreignBlacklistedSrcTxId = await warp.saveSource(foreignBlacklistContractSrcTx); + const foreignWhitelistSrcTx = await warp.createSource({ src: foreignContractSrc }, wallet); + foreignWhitelistedSrcTxId = await warp.saveSource(foreignWhitelistSrcTx); + + const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8')); + + initialState = { + ...stateFromFile, + ...{ + owner: walletAddress, + balances: { + ...stateFromFile.balances, + [walletAddress]: 555669 + } + } + }; + + ({ contractTxId } = await warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId + })); + + ({ contractTxId: foreignBlacklistedContractTxId } = await warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId: foreignBlacklistedSrcTxId + })); + + ({ contractTxId: foreignWhitelistedContractTxId } = await warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId: foreignWhitelistedSrcTxId + })); + + pst = warp.pst(contractTxId).setEvaluationOptions({ + internalWrites: true, + whitelistSources: [srcTxId, foreignWhitelistedSrcTxId] + }) as PstContract; + pst.connect(wallet); + + foreignWhitelistedPst = warp + .pst(foreignWhitelistedContractTxId) + .setEvaluationOptions({ + internalWrites: true + }) + .connect(wallet) as PstContract; + + foreignBlacklistedPst = warpBlacklisted + .pst(foreignBlacklistedContractTxId) + .setEvaluationOptions({ + internalWrites: true + }) + .connect(wallet) as PstContract; + }); + + afterAll(async () => { + await arlocal.stop(); + }); + + it('should properly transfer tokens', async () => { + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 555 + }); + + expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 555); + expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 555); + }); + + it('should properly perform write from foreign whitelisted contract', async () => { + await foreignWhitelistedPst.writeInteraction({ + function: 'writeForeign', + contractTxId: contractTxId + }); + + const result = await pst.readState(); + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1); + }); + + it('should block write from foreign blacklisted contract (1)', async () => { + const blacklistedWriteTx = await foreignBlacklistedPst.writeInteraction({ + function: 'writeForeign', + contractTxId: contractTxId + }); + + const result = await pst.readState(); + expect(result.cachedValue.validity[blacklistedWriteTx.originalTxId]).toBeFalsy(); + // should not change from previous test + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1); + }); + + it('should block write from foreign blacklisted contract (2)', async () => { + const blacklistedWriteTx = await foreignBlacklistedPst.writeInteraction({ + function: 'writeForeign', + contractTxId: contractTxId + }); + + const result = await pst.readState(); + expect(result.cachedValue.validity[blacklistedWriteTx.originalTxId]).toBeFalsy(); + // should not change from previous test + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1); + }); + + it('should block write from foreign whitelisted contract that evolved to blacklisted', async () => { + const srcTx = await warp.createSource({ src: foreignContractSrc }, wallet); + const nonWhitelistedSrcTxId = await warp.saveSource(srcTx); + + await foreignWhitelistedPst.evolve(nonWhitelistedSrcTxId); + + const blacklistedWriteTx = await foreignWhitelistedPst.writeInteraction({ + function: 'writeForeign', + contractTxId: contractTxId + }); + + const result = await pst.readState(); + expect(result.cachedValue.validity[blacklistedWriteTx.originalTxId]).toBeFalsy(); + // should not change from previous test + expect((result.cachedValue.state as any).foreignCallsCounter).toEqual(1); + }); +}); diff --git a/src/__tests__/integration/basic/whitelist-sources.test.ts b/src/__tests__/integration/basic/whitelist-sources.test.ts new file mode 100644 index 0000000..8031792 --- /dev/null +++ b/src/__tests__/integration/basic/whitelist-sources.test.ts @@ -0,0 +1,94 @@ +import fs from 'fs'; + +import ArLocal from 'arlocal'; +import Arweave from 'arweave'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import path from 'path'; +import { PstContract, PstState } from '../../../contract/PstContract'; +import { Warp } from '../../../core/Warp'; +import { WarpFactory } from '../../../core/WarpFactory'; +import { LoggerFactory } from '../../../logging/LoggerFactory'; +import { DeployPlugin } from 'warp-contracts-plugin-deploy'; + +describe('Testing whitelist sources in nested contracts', () => { + let contractSrc, foreignContractSrc: string; + + let wallet: JWKInterface; + let walletAddress: string; + + let initialState: PstState; + + let arweave: Arweave; + let arlocal: ArLocal; + let warp: Warp; + let mainSrcTxId: string; + let pst, pstWhitelisted, pstBlacklisted: PstContract; + + beforeAll(async () => { + arlocal = new ArLocal(1900, false); + await arlocal.start(); + LoggerFactory.INST.logLevel('error'); + warp = WarpFactory.forLocal(1900).use(new DeployPlugin()); + + ({ arweave } = warp); + ({ jwk: wallet, address: walletAddress } = await warp.generateWallet()); + + contractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8'); + const mainSrcTx = await warp.createSource({ src: contractSrc }, wallet); + mainSrcTxId = await warp.saveSource(mainSrcTx); + + foreignContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-foreign.js'), 'utf8'); + const foreignSrcTx = await warp.createSource({ src: foreignContractSrc }, wallet); + const foreignSrcTxId = await warp.saveSource(foreignSrcTx); + + 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 warp.deployFromSourceTx({ + wallet, + initState: JSON.stringify(initialState), + srcTxId: mainSrcTxId + }); + + pst = warp.pst(contractTxId) as PstContract; + pst.connect(wallet); + + pstWhitelisted = warp.pst(contractTxId).setEvaluationOptions({ + whitelistSources: [mainSrcTxId] + }) as PstContract; + pstWhitelisted.connect(wallet); + + pstBlacklisted = warp.pst(contractTxId).setEvaluationOptions({ + whitelistSources: [foreignSrcTxId] + }) as PstContract; + pstBlacklisted.connect(wallet); + }); + + afterAll(async () => { + await arlocal.stop(); + }); + + it('should allow to evaluate contract by default', async () => { + expect(await pst.readState()).toBeDefined(); + }); + + it('should allow to evaluate contract when src tx id is in the whitelist', async () => { + expect(await pstWhitelisted.readState()).toBeDefined(); + }); + + it('should not allow to evaluate contract when src tx id is in the whitelist', async () => { + await expect(pstBlacklisted.readState()).rejects.toThrowError( + `[NonWhitelistedSourceError] Contract source not part of whitelisted sources list: ${mainSrcTxId}.` + ); + }); +}); diff --git a/src/__tests__/integration/data/token-pst-foreign.js b/src/__tests__/integration/data/token-pst-foreign.js new file mode 100644 index 0000000..4f08a14 --- /dev/null +++ b/src/__tests__/integration/data/token-pst-foreign.js @@ -0,0 +1,73 @@ +export async function handle(state, action) { + const balances = state.balances; + const canEvolve = state.canEvolve; + const input = action.input; + const caller = action.caller; + + if (input.function === 'transfer') { + const target = input.target; + const qty = input.qty; + + if (!Number.isInteger(qty)) { + throw new ContractError('Invalid value for "qty". Must be an integer'); + } + + if (!target) { + throw new ContractError('No target specified'); + } + + if (qty <= 0 || caller === target) { + throw new ContractError('Invalid token transfer'); + } + + if (balances[caller] < qty) { + throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`); + } + + // Lower the token balance of the caller + balances[caller] -= qty; + if (target in balances) { + // Wallet already exists in state, add new tokens + balances[target] += qty; + } else { + // Wallet is new, set starting balance + balances[target] = qty; + } + + return { state }; + } + + if (input.function === 'balance') { + const target = input.target; + const ticker = state.ticker; + + if (typeof target !== 'string') { + throw new ContractError('Must specify target to get balance for'); + } + + if (typeof balances[target] !== 'number') { + throw new ContractError('Cannot get balance, target does not exist'); + } + + return { result: { target, ticker, balance: balances[target] } }; + } + + if (input.function === 'evolve' && canEvolve) { + if (state.owner !== caller) { + throw new ContractError('Only the owner can evolve a contract.'); + } + + state.evolve = input.value; + + return { state }; + } + + if (input.function === 'writeForeign') { + const result = await SmartWeave.contracts.write(input.contractTxId, { + function: "callFromForeign" + }); + return {state}; + } + + throw new ContractError(`No function supplied or function not recognised: "${input.function}"`); +} diff --git a/src/__tests__/unit/evaluation-options.test.ts b/src/__tests__/unit/evaluation-options.test.ts index d882477..97e266e 100644 --- a/src/__tests__/unit/evaluation-options.test.ts +++ b/src/__tests__/unit/evaluation-options.test.ts @@ -30,7 +30,8 @@ describe('Evaluation options evaluator', () => { useKVStorage: false, waitForConfirmation: false, useConstructor: false, - walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' + walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/', + whitelistSources: [] }); contract.setEvaluationOptions({ @@ -67,7 +68,8 @@ describe('Evaluation options evaluator', () => { useKVStorage: false, waitForConfirmation: false, useConstructor: false, - walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' + walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/', + whitelistSources: [] }); const contract2 = warp.contract(null).setEvaluationOptions({ @@ -99,7 +101,8 @@ describe('Evaluation options evaluator', () => { useKVStorage: false, waitForConfirmation: false, useConstructor: false, - walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' + walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/', + whitelistSources: [] }); expect( @@ -128,7 +131,8 @@ describe('Evaluation options evaluator', () => { useKVStorage: false, waitForConfirmation: false, useConstructor: false, - walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' + walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/', + whitelistSources: [] }); expect( @@ -157,7 +161,8 @@ describe('Evaluation options evaluator', () => { useKVStorage: false, waitForConfirmation: false, useConstructor: false, - walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' + walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/', + whitelistSources: [] }); expect( @@ -186,7 +191,8 @@ describe('Evaluation options evaluator', () => { useKVStorage: false, waitForConfirmation: false, useConstructor: false, - walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' + walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/', + whitelistSources: [] }); const contract3 = warp.contract(null).setEvaluationOptions({ diff --git a/src/__tests__/unit/gateway-interactions.loader.test.ts b/src/__tests__/unit/gateway-interactions.loader.test.ts index f1e060f..74ba656 100644 --- a/src/__tests__/unit/gateway-interactions.loader.test.ts +++ b/src/__tests__/unit/gateway-interactions.loader.test.ts @@ -19,54 +19,46 @@ const responseData = { }, interactions: [ { - status: 'confirmed', - confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57', - confirmations: '172044,172044,172044', - interaction: { - id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg', - fee: { - winston: '48173811033' - }, - tags: [], - block: { - id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY', - height: 655393, - timestamp: 1617060107 - }, - owner: { - address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0' - }, - parent: null, - quantity: { - winston: '0' - }, - recipient: '' - } + id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg', + fee: { + winston: '48173811033' + }, + tags: [], + block: { + id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY', + height: 655393, + timestamp: 1617060107 + }, + owner: { + address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0' + }, + parent: null, + quantity: { + winston: '0' + }, + recipient: '', + sortKey: '000000645844,0000000000000,4e49e10a3c76445b00501b704e9caab118c14ad56694a16e7e4c43c2c142e006' }, { - status: 'confirmed', - confirming_peers: '94.130.135.178,159.203.49.13,95.217.114.57', - confirmations: '172044,172044,172044', - interaction: { - id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg', - fee: { - winston: '48173811033' - }, - tags: [], - block: { - id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY', - height: 655393, - timestamp: 1617060107 - }, - owner: { - address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0' - }, - parent: null, - quantity: { - winston: '0' - }, - recipient: '' - } + id: 'XyJm1OERe__Q-YcwTQrCeYsI14_ylASey6eYdPg-HYg', + fee: { + winston: '48173811033' + }, + tags: [], + block: { + id: 'w8y2bxCQd3-26lvvy2NOt6Qz0kVooN9h4rwy6UIeC5mEfVnbftqcnWEavZfT14vY', + height: 655393, + timestamp: 1617060107 + }, + owner: { + address: 'oZjQWwcTYbEvnwr6zkxFqpEoDTPvWkaL3zO3-SFq2g0' + }, + parent: null, + quantity: { + winston: '0' + }, + recipient: '', + sortKey: '000000662481,0000000000000,82ef246cdc8be74447260bcbf44c21239f8ee7a36af51b29c3dc714bcefb0509' } ] }; diff --git a/src/contract/EvaluationOptionsEvaluator.ts b/src/contract/EvaluationOptionsEvaluator.ts index a5a86bf..477a2ce 100644 --- a/src/contract/EvaluationOptionsEvaluator.ts +++ b/src/contract/EvaluationOptionsEvaluator.ts @@ -106,7 +106,8 @@ export class EvaluationOptionsEvaluator { remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'], remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'], useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'], - useConstructor: (foreignOptions) => foreignOptions['useConstructor'] + useConstructor: (foreignOptions) => foreignOptions['useConstructor'], + whitelistSources: () => this.rootOptions['whitelistSources'] }; private readonly notConflictingEvaluationOptions: (keyof EvaluationOptions)[] = [ diff --git a/src/core/modules/StateEvaluator.ts b/src/core/modules/StateEvaluator.ts index da1e652..4aa42d2 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -150,6 +150,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions { remoteStateSyncSource = 'https://dre-1.warp.cc/contract'; useConstructor = false; + + whitelistSources = []; } // an interface for the contract EvaluationOptions - can be used to change the behaviour of some features. @@ -238,4 +240,6 @@ export interface EvaluationOptions { // remote source for fetching most recent contract state, only applicable if remoteStateSyncEnabled is set to true remoteStateSyncSource: string; + + whitelistSources: string[]; } diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index b64effc..86bb17c 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -13,6 +13,7 @@ import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExe import { TagsParser } from './TagsParser'; import { VrfPluginFunctions } from '../../WarpPlugin'; import { BasicSortKeyCache } from '../../../cache/BasicSortKeyCache'; +import { KnownErrors } from './handler/JsHandlerApi'; type EvaluationProgressInput = { contractTxId: string; @@ -155,8 +156,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { } catch (e) { // ppe: not sure why we're not handling all ContractErrors here... if ( - e.name == 'ContractError' && - (e.subtype == 'unsafeClientSkip' || e.subtype == 'constructor' || e.subtype == 'blacklistedSkip') + (e.name == KnownErrors.ContractError && + (e.subtype == 'unsafeClientSkip' || e.subtype == 'constructor' || e.subtype == 'blacklistedSkip')) || + e.name == KnownErrors.NonWhitelistedSourceError ) { this.logger.warn(`Skipping contract in internal write, reason ${e.subtype}`); errorMessages[missingInteraction.id] = e; @@ -282,7 +284,10 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { executionContext = await modify(currentState, executionContext); } } catch (e) { - if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') { + if ( + (e.name == KnownErrors.ContractError && e.subtype == 'unsafeClientSkip') || + e.name == KnownErrors.NonWhitelistedSourceError + ) { validity[missingInteraction.id] = false; errorMessages[missingInteraction.id] = e.message; shouldBreakAfterEvolve = true; diff --git a/src/core/modules/impl/HandlerExecutorFactory.ts b/src/core/modules/impl/HandlerExecutorFactory.ts index 91ce4ea..6673d4d 100644 --- a/src/core/modules/impl/HandlerExecutorFactory.ts +++ b/src/core/modules/impl/HandlerExecutorFactory.ts @@ -8,7 +8,7 @@ import { Benchmark } from '../../../logging/Benchmark'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { ExecutorFactory } from '../ExecutorFactory'; import { EvalStateResult, EvaluationOptions } from '../StateEvaluator'; -import { JsHandlerApi } from './handler/JsHandlerApi'; +import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi'; import { WasmHandlerApi } from './handler/WasmHandlerApi'; import { normalizeContractSource } from './normalize-source'; import { Warp } from '../../Warp'; @@ -25,7 +25,14 @@ const BigNumber = require('bignumber.js'); export class ContractError extends Error { constructor(readonly error: T, readonly subtype?: string) { super(error.toString()); - this.name = 'ContractError'; + this.name = KnownErrors.ContractError; + } +} + +export class NonWhitelistedSourceError extends Error { + constructor(readonly error: T) { + super(error.toString()); + this.name = KnownErrors.NonWhitelistedSourceError; } } @@ -45,20 +52,18 @@ export class HandlerExecutorFactory implements ExecutorFactory> { if (warp.hasPlugin('contract-blacklist')) { - const blacklistPlugin = warp.loadPlugin>('contract-blacklist'); - let blacklisted = false; - try { - blacklisted = await blacklistPlugin.process(contractDefinition.txId); - } catch (e) { - this.logger.error(e); - } - if (blacklisted == true) { - throw new ContractError( - `[SkipUnsafeError] Skipping evaluation of the blacklisted contract ${contractDefinition.txId}.`, - `blacklistedSkip` - ); - } + await this.blacklistContracts(warp, contractDefinition); } + + if ( + evaluationOptions.whitelistSources.length > 0 && + !evaluationOptions.whitelistSources.includes(contractDefinition.srcTxId) + ) { + throw new NonWhitelistedSourceError( + `[NonWhitelistedSourceError] Contract source not part of whitelisted sources list: ${contractDefinition.srcTxId}.` + ); + } + let kvStorage = null; if (evaluationOptions.useKVStorage) { @@ -213,6 +218,22 @@ export class HandlerExecutorFactory implements ExecutorFactory(warp: Warp, contractDefinition: ContractDefinition) { + const blacklistPlugin = warp.loadPlugin>('contract-blacklist'); + let blacklisted = false; + try { + blacklisted = await blacklistPlugin.process(contractDefinition.txId); + } catch (e) { + this.logger.error(e); + } + if (blacklisted == true) { + throw new ContractError( + `[SkipUnsafeError] Skipping evaluation of the blacklisted contract ${contractDefinition.txId}.`, + `blacklistedSkip` + ); + } + } } function generateResponse(wasmBinary: Buffer): Response { diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index 63050ef..4fce623 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -143,7 +143,12 @@ export abstract class AbstractContractHandler implements HandlerApi { export enum KnownErrors { ContractError = 'ContractError', ConstructorError = 'ConstructorError', - NetworkCommunicationError = 'NetworkCommunicationError' + NetworkCommunicationError = 'NetworkCommunicationError', + NonWhitelistedSourceError = 'NonWhitelistedSourceError' } export class JsHandlerApi extends AbstractContractHandler { @@ -186,6 +187,13 @@ export class JsHandlerApi extends AbstractContractHandler { // any network-based error should result in immediately stop contract evaluation case KnownErrors.NetworkCommunicationError: throw err; + case KnownErrors.NonWhitelistedSourceError: + return { + type: 'error' as const, + errorMessage: err.message, + state: state, + result: null + }; default: return { type: 'exception' as const, diff --git a/src/core/modules/impl/handler/WasmHandlerApi.ts b/src/core/modules/impl/handler/WasmHandlerApi.ts index 17d65b0..1fd0343 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -3,7 +3,13 @@ import { ContractDefinition } from '../../../../core/ContractDefinition'; import { ExecutionContext } from '../../../../core/ExecutionContext'; import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global'; -import { ContractError, ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory'; +import { + ContractError, + ContractInteraction, + InteractionData, + InteractionResult, + NonWhitelistedSourceError +} from '../HandlerExecutorFactory'; import { AbstractContractHandler } from './AbstractContractHandler'; import { NetworkCommunicationError } from "../../../../utils/utils"; @@ -58,7 +64,7 @@ export class WasmHandlerApi extends AbstractContractHandler { state: currentResult.state, result: null }; - if (e instanceof ContractError) { + if (e instanceof ContractError || e instanceof NonWhitelistedSourceError) { return { ...result, error: e.error, diff --git a/src/plugins/Evolve.ts b/src/plugins/Evolve.ts index bfc64c0..ca3572a 100644 --- a/src/plugins/Evolve.ts +++ b/src/plugins/Evolve.ts @@ -4,6 +4,7 @@ import { ExecutionContext } from '../core/ExecutionContext'; import { ExecutionContextModifier } from '../core/ExecutionContextModifier'; import { SmartWeaveError, SmartWeaveErrorType } from '../legacy/errors'; import { HandlerApi } from '../core/modules/impl/HandlerExecutorFactory'; +import { KnownErrors } from '../core/modules/impl/handler/JsHandlerApi'; function isEvolveCompatible(state: unknown): state is EvolveState { if (!state) { @@ -58,7 +59,10 @@ export class Evolve implements ExecutionContextModifier { return executionContext; } catch (e) { - if (e.name === 'ContractError' && e.subtype === 'unsafeClientSkip') { + if ( + (e.name === KnownErrors.ContractError && e.subtype === 'unsafeClientSkip') || + e.name == KnownErrors.NonWhitelistedSourceError + ) { throw e; } else { throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, {