feat: whitelist contract sources (#449)

* feat: whitelist contract sources

* feat: sort interactions loaded from Warp Gateway
This commit is contained in:
Asia
2023-08-02 17:39:36 +02:00
committed by just_ppe
parent 37f325ef3b
commit d3341e8128
15 changed files with 753 additions and 76 deletions

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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}.`
);
});
});

View File

@@ -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}"`);
}

View File

@@ -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({

View File

@@ -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'
}
]
};

View File

@@ -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)[] = [

View File

@@ -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[];
}

View File

@@ -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<State>(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;

View File

@@ -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<T> extends Error {
constructor(readonly error: T, readonly subtype?: string) {
super(error.toString());
this.name = 'ContractError';
this.name = KnownErrors.ContractError;
}
}
export class NonWhitelistedSourceError<T> extends Error {
constructor(readonly error: T) {
super(error.toString());
this.name = KnownErrors.NonWhitelistedSourceError;
}
}
@@ -45,20 +52,18 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
interactionState: InteractionState
): Promise<HandlerApi<State>> {
if (warp.hasPlugin('contract-blacklist')) {
const blacklistPlugin = warp.loadPlugin<string, Promise<boolean>>('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<State>(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<HandlerApi<unknow
}
}
}
private async blacklistContracts<State>(warp: Warp, contractDefinition: ContractDefinition<State>) {
const blacklistPlugin = warp.loadPlugin<string, Promise<boolean>>('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 {

View File

@@ -143,7 +143,12 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
const lastErrorMessage = stateWithValidity?.cachedValue?.errorMessages[lastErrorKey];
// don't judge me..
// FIXME: also - '?' is stinky...
if (lastErrorMessage?.startsWith('[SkipUnsafeError]')) {
if (
lastErrorMessage &&
lastErrorMessage.startsWith &&
(lastErrorMessage.startsWith('[SkipUnsafeError]') ||
lastErrorMessage.startsWith('[NonWhitelistedSourceError]'))
) {
throw new ContractError(lastErrorMessage);
}
}

View File

@@ -17,7 +17,8 @@ const throwErrorWithName = (name: string, message: string) => {
export enum KnownErrors {
ContractError = 'ContractError',
ConstructorError = 'ConstructorError',
NetworkCommunicationError = 'NetworkCommunicationError'
NetworkCommunicationError = 'NetworkCommunicationError',
NonWhitelistedSourceError = 'NonWhitelistedSourceError'
}
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
@@ -186,6 +187,13 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
// 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,

View File

@@ -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<State> extends AbstractContractHandler<State> {
state: currentResult.state,
result: null
};
if (e instanceof ContractError) {
if (e instanceof ContractError || e instanceof NonWhitelistedSourceError) {
return {
...result,
error: e.error,

View File

@@ -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, {