[FEATURE] Whitelisting contracts for internal writes #403

This commit is contained in:
ppedziwiatr
2023-05-08 10:55:11 +02:00
parent 1a1d90d044
commit bc7ba106e3
11 changed files with 2000 additions and 27 deletions

View File

@@ -16,6 +16,7 @@ import {
} from '../../..';
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
import path from 'path';
import { WritesAware } from '../../../src';
jest.setTimeout(30000);
@@ -42,7 +43,7 @@ describe('Testing the Rust WASM Profit Sharing Token', () => {
let arweave: Arweave;
let arlocal: ArLocal;
let warp: Warp;
let toyContract: Contract<State>;
let toyContract: Contract<State & WritesAware>;
let contractTxId: string;

View File

@@ -112,11 +112,13 @@
"json-schema-to-typescript": "^11.0.1",
"node-stdlib-browser": "^1.2.0",
"prettier": "^2.3.2",
"ramda": "^0.29.0",
"rimraf": "^3.0.2",
"smartweave": "0.4.48",
"ts-jest": "^28.0.7",
"ts-node": "^10.2.1",
"typescript": "^4.9.5",
"warp-contracts": "^1.4.5",
"warp-contracts-plugin-deploy": "1.0.8-beta.0",
"warp-contracts-plugin-vm2": "1.0.0",
"warp-contracts-plugin-vrf": "^1.0.3",

View File

@@ -51,4 +51,12 @@ export async function handle(state, action) {
if (action.input.function === 'justThrow') {
throw new ContractError('Error from justThrow function');
}
if (action.input.function === 'setAllowedSrc') {
const allowedSrc = action.input.allowedSrc;
state.allowedSrcTxIds = allowedSrc;
return { state };
}
}

View File

@@ -0,0 +1,217 @@
/* eslint-disable */
import fs from "fs";
import ArLocal from "arlocal";
import { JWKInterface } from "arweave/node/lib/wallet";
import path from "path";
import { mineBlock } from "../../_helpers";
import { Contract, WritesAware } from "../../../../contract/Contract";
import { Warp } from "../../../../core/Warp";
import { WarpFactory } from "../../../../core/WarpFactory";
import { LoggerFactory } from "../../../../logging/LoggerFactory";
import { DeployPlugin } from "warp-contracts-plugin-deploy";
import Transaction from "arweave/node/lib/transaction";
import { WARP_TAGS } from "../../../../core/KnownTags";
import { createInteractionTx } from "../../../../legacy/create-interaction-tx";
import { Signature } from "../../../../contract/Signature";
interface ExampleContractState {
counter: number;
errorCounter: number;
}
type CalleeState = ExampleContractState & WritesAware;
describe("Testing internal writes with whitelist", () => {
let callingContractSrc: string;
let callingContractInitialState: string;
let calleeContractSrc: string;
let calleeInitialState: string;
let wallet: JWKInterface;
let arlocal: ArLocal;
let warp: Warp;
let calleeContract: Contract<CalleeState>;
let callingContract: Contract<ExampleContractState>;
let calleeTxId;
let callingTxId;
let callingSrcTxId;
const port = 1289;
beforeAll(async () => {
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(port, false);
await arlocal.start();
LoggerFactory.INST.logLevel("error");
warp = WarpFactory.forLocal(port).use(new DeployPlugin());
({ jwk: wallet } = await warp.generateWallet());
});
afterAll(async () => {
await arlocal.stop();
});
async function deployContracts() {
callingContractSrc = fs.readFileSync(path.join(__dirname, "../../data/writing-contract.js"), "utf8");
callingContractInitialState = fs.readFileSync(path.join(__dirname, "../../data/writing-contract-state.json"), "utf8");
calleeContractSrc = fs.readFileSync(path.join(__dirname, "../../data/example-contract.js"), "utf8");
calleeInitialState = fs.readFileSync(path.join(__dirname, "../../data/example-contract-state.json"), "utf8");
({ contractTxId: callingTxId, srcTxId: callingSrcTxId } = await warp.deploy({
wallet,
initState: callingContractInitialState,
src: callingContractSrc
}));
callingContract = warp
.contract<ExampleContractState>(callingTxId)
.setEvaluationOptions({
internalWrites: true,
mineArLocalBlocks: false
})
.connect(wallet);
await mineBlock(warp);
}
beforeAll(async () => {
await deployContracts();
({ contractTxId: calleeTxId } = await warp.deploy({
wallet,
initState: JSON.stringify({
allowedSrcTxIds: [],
...JSON.parse(calleeInitialState)
}),
src: calleeContractSrc
}));
calleeContract = warp
.contract<CalleeState>(calleeTxId)
.setEvaluationOptions({
internalWrites: true,
mineArLocalBlocks: false
})
.connect(wallet);
await mineBlock(warp);
});
it("should block internal write on creation in strict mode", async () => {
await expect(callingContract.writeInteraction({
function: "writeContract",
contractId: calleeTxId,
amount: 10
}, { strict: true }))
.rejects.toThrowError("[WriteNotAllowed]");
});
it("should skip evaluation of the inner write tx", async () => {
await calleeContract.writeInteraction({ function: "add" });
await mineBlock(warp);
const invalidTx2 = await callingContract.writeInteraction({
function: "writeContract",
contractId: calleeTxId,
amount: 10
});
await mineBlock(warp);
await calleeContract.writeInteraction({ function: "add" });
await mineBlock(warp);
const result = await calleeContract.readState();
expect(result.cachedValue.validity[invalidTx2.originalTxId]).toBeUndefined();
});
it("should allow evaluation after adding to allowed array", async () => {
await calleeContract.writeInteraction(
{
function: "setAllowedSrc",
allowedSrc: [callingSrcTxId]
});
await mineBlock(warp);
const writeTx = await callingContract.writeInteraction({
function: "writeContract",
contractId: calleeTxId,
amount: 10
});
await mineBlock(warp);
const result = await calleeContract.readState();
expect(result.cachedValue.validity[writeTx.originalTxId]).toBeTruthy();
expect(result.cachedValue.state.counter).toEqual(567);
});
it("should block writes made outside of the SDK if whitelist empty", async () => {
// clear the white list
await calleeContract.writeInteraction(
{
function: "setAllowedSrc",
allowedSrc: []
});
await mineBlock(warp);
const hackedTx = await createInteractionTx(
warp.arweave,
(new Signature(warp, wallet)).signer,
callingTxId,
{
function: "writeContract",
contractId: calleeTxId,
amount: 10
},
[{name: WARP_TAGS.INTERACT_WRITE, value: calleeTxId}],
'',
'0',
false,
false
);
const response = await warp.arweave.transactions.post(hackedTx);
expect(response.status).toEqual(200);
await mineBlock(warp);
const result = await calleeContract.readState();
expect(result.cachedValue.validity[hackedTx.id]).toBeFalsy();
expect(result.cachedValue.errorMessages[hackedTx.id]).toContain('[WriteNotAllowed]');
});
it("should allow writes made outside of the SDK if whitelist non-empty", async () => {
// add the white list
await calleeContract.writeInteraction(
{
function: "setAllowedSrc",
allowedSrc: [callingSrcTxId]
});
await mineBlock(warp);
const hackedTx = await createInteractionTx(
warp.arweave,
(new Signature(warp, wallet)).signer,
callingTxId,
{
function: "writeContract",
contractId: calleeTxId,
amount: 10
},
[{name: WARP_TAGS.INTERACT_WRITE, value: calleeTxId}],
'',
'0',
false,
false
);
const response = await warp.arweave.transactions.post(hackedTx);
expect(response.status).toEqual(200);
await mineBlock(warp);
const result = await calleeContract.readState();
expect(result.cachedValue.validity[hackedTx.id]).toBeTruthy();
expect(result.cachedValue.state.counter).toEqual(577);
});
});

View File

@@ -7,6 +7,7 @@ import { ArTransfer, Tags, ArWallet } from './deploy/CreateContract';
import { CustomSignature } from './Signature';
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { InteractionState } from './states/InteractionState';
import { ContractDefinition } from '../core/ContractDefinition';
export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: number; total: number };
@@ -71,6 +72,10 @@ export type InnerCallType = 'read' | 'view' | 'write';
export type InnerCallData = { callingInteraction: GQLNodeInterface; callType: InnerCallType };
export type WritesAware = {
allowedSrcTxIds?: string[];
};
/**
* A base interface to be implemented by SmartWeave Contracts clients
* - contains "low-level" methods that allow to interact with any contract
@@ -150,7 +155,8 @@ export interface Contract<State = unknown> {
*/
viewStateForTx<Input = unknown, View = unknown>(
input: Input,
transaction: GQLNodeInterface
transaction: GQLNodeInterface,
contractDefinition: ContractDefinition<State>
): Promise<InteractionResult<State, View>>;
/**
@@ -172,7 +178,17 @@ export interface Contract<State = unknown> {
vrf?: boolean
): Promise<InteractionResult<State, unknown>>;
applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>>;
/**
* Applies input on the current state of the contract.
* Verifies whether the callee contract has the calling contract whitelisted (https://github.com/warp-contracts/warp/issues/403).
* @param input
* @param transaction
*/
applyInputSafe<Input>(
input: Input,
transaction: GQLNodeInterface,
contractDef: ContractDefinition<State>
): Promise<InteractionResult<State, unknown>>;
/**
* Writes a new "interaction" transaction - i.e. such transaction that stores input for the contract.

View File

@@ -20,7 +20,7 @@ import { Benchmark } from '../logging/Benchmark';
import { LoggerFactory } from '../logging/LoggerFactory';
import { Evolve } from '../plugins/Evolve';
import { ArweaveWrapper } from '../utils/ArweaveWrapper';
import { getJsonResponse, sleep, stripTrailingSlash } from '../utils/utils';
import { getJsonResponse, isWritesWhitelistAware, sleep, stripTrailingSlash } from '../utils/utils';
import {
BenchmarkStats,
Contract,
@@ -40,11 +40,11 @@ import { InteractionState } from './states/InteractionState';
import { ContractInteractionState } from './states/ContractInteractionState';
import { Crypto } from 'warp-isomorphic';
import { VrfPluginFunctions } from '../core/WarpPlugin';
import Arweave from 'arweave';
import { ContractDefinition } from '../core/ContractDefinition';
/**
* An implementation of {@link Contract} that is backwards compatible with current style
* of writing SW contracts (ie. using the "handle" function).
* of writing SW contracts (i.e. using the "handle" function).
*
* It requires {@link ExecutorFactory} that is using {@link HandlerApi} generic type.
*/
@@ -215,10 +215,11 @@ export class HandlerBasedContract<State> implements Contract<State> {
async viewStateForTx<Input, View>(
input: Input,
interactionTx: GQLNodeInterface
transaction: GQLNodeInterface,
contractDefinition: ContractDefinition<State>
): Promise<InteractionResult<State, View>> {
this.logger.info(`View state for ${this._contractTxId}`);
return await this.doApplyInputOnTx<Input, View>(input, interactionTx, 'view');
return await this.doApplyInputOnTx<Input, View>(input, transaction, 'view', contractDefinition);
}
async dryWrite<Input>(
@@ -232,9 +233,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
return await this.callContract<Input>(input, 'write', caller, undefined, tags, transfer, undefined, vrf);
}
async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
async applyInputSafe<Input>(
input: Input,
transaction: GQLNodeInterface,
contractDef: ContractDefinition<State>
): Promise<InteractionResult<State, unknown>> {
this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`);
return await this.doApplyInputOnTx<Input>(input, transaction, 'write');
return await this.doApplyInputOnTx<Input>(input, transaction, 'write', contractDef);
}
async writeInteraction<Input>(
@@ -290,6 +295,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
effectiveVrf && environment !== 'mainnet',
effectiveReward
);
const response = await arweave.transactions.post(interactionTx);
if (response.status !== 200) {
@@ -689,7 +695,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
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<void, VrfPluginFunctions>('vrf');
if (vrfPlugin) {
dummyTx.vrf = vrfPlugin.process().generateMockVrf(dummyTx.sortKey);
@@ -720,7 +725,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
private async doApplyInputOnTx<Input, View = unknown>(
input: Input,
interactionTx: GQLNodeInterface,
interactionType: InteractionType
interactionType: InteractionType,
callingContractDef: ContractDefinition<State>
): Promise<InteractionResult<State, View>> {
this.maybeResetRootContract();
@@ -738,6 +744,30 @@ export class HandlerBasedContract<State> implements Contract<State> {
this.interactionState().update(this.txId(), evalStateResult.cachedValue);
}
const calleeState = evalStateResult.cachedValue.state;
if (interactionType === 'write' && isWritesWhitelistAware(calleeState)) {
let errorMessage = null;
if (calleeState.allowedSrcTxIds.length === 0) {
errorMessage = '[WriteNotAllowed] Internal writes not allowed.';
} else {
const callingSrcTxId = callingContractDef.srcTxId;
if (!calleeState.allowedSrcTxIds.includes(callingSrcTxId)) {
errorMessage = `[WriteNotAllowed] Calling contract source ${callingSrcTxId} not allowed.`;
}
}
if (errorMessage) {
return {
type: 'error',
errorMessage,
originalValidity: evalStateResult.cachedValue.validity,
originalErrorMessages: evalStateResult.cachedValue.errorMessages,
state: calleeState,
result: null
};
}
}
this.logger.debug('callContractForTx - evalStateResult', {
result: evalStateResult.cachedValue.state,
txId: this._contractTxId

View File

@@ -63,7 +63,11 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
callingInteraction: this.swGlobal._activeTx,
callType: 'write'
});
const result = await calleeContract.applyInput<Input>(input, this.swGlobal._activeTx);
const result = await calleeContract.applyInputSafe<Input>(
input,
this.swGlobal._activeTx,
this.contractDefinition
);
this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry);
const shouldAutoThrow =
@@ -110,7 +114,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
callType: 'view'
});
return await childContract.viewStateForTx<Input, View>(input, this.swGlobal._activeTx);
return await childContract.viewStateForTx<Input, View>(input, this.swGlobal._activeTx, this.contractDefinition);
};
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable */
import copy from 'fast-copy';
import { Buffer } from 'warp-isomorphic';
import { WritesAware } from "../contract/Contract";
export const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -102,3 +103,11 @@ export async function getJsonResponse<T>(response: Promise<Response>): Promise<T
const result = await r.json();
return result as T;
}
export function isWritesWhitelistAware(state: unknown): state is WritesAware {
if (!state) {
return false;
}
return Array.isArray((state as WritesAware).allowedSrcTxIds);
}

View File

@@ -7,7 +7,7 @@ import { JWKInterface } from 'arweave/node/lib/wallet';
import {ArweaveSigner, DeployPlugin} from "warp-contracts-plugin-deploy";
async function main() {
let wallet: JWKInterface = readJSON('./.secrets/33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA.json');
let wallet: JWKInterface = readJSON('./.secrets/warp.json');
LoggerFactory.INST.logLevel('error');
//LoggerFactory.INST.logLevel('debug', 'ExecutionContext');
const logger = LoggerFactory.INST.create('deploy');
@@ -20,26 +20,25 @@ async function main() {
try {
const warp = WarpFactory.forMainnet({...defaultCacheOptions, inMemory: true})
.use(new DeployPlugin())
.useGwUrl("http://localhost:5666/");
.use(new DeployPlugin());
const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.js'), 'utf8');
const initialState = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.json'), 'utf8');
// case 1 - full deploy, js contract
/* const { contractTxId, srcTxId } = await warp.deploy({
wallet: new ArweaveSigner(wallet),
const { contractTxId, srcTxId } = await warp.deploy({
wallet: wallet,
initState: initialState,
src: jsContractSrc
/!*evaluationManifest: {
/*evaluationManifest: {
evaluationOptions: {
useKVStorage: true
}
}*!/
});
}*/
}, true);
console.log('contractTxId:', contractTxId);
console.log('srcTxId:', srcTxId);*/
console.log('srcTxId:', srcTxId);
// case 2 - deploy from source, js contract
/*const {contractTxId} = await warp.createContract.deployFromSourceTx({
wallet,
@@ -63,9 +62,9 @@ async function main() {
srcTxId: "5wXT-A0iugP9pWEyw-iTbB0plZ_AbmvlNKyBfGS3AUY",
});*/
const contract = warp.contract<any>('SG9sKOZvKFQ7EcpJU3bS0pQWp2idQf3VY2Ki_5-hDjo').setEvaluationOptions({
/*const contract = warp.contract<any>('SG9sKOZvKFQ7EcpJU3bS0pQWp2idQf3VY2Ki_5-hDjo').setEvaluationOptions({
sequencerUrl: 'http://localhost:5666/'
}).connect(wallet);
}).connect(wallet);*/
await Promise.all([
/* contract.writeInteraction<any>({
@@ -86,7 +85,7 @@ async function main() {
disableBundling: true
})*/
]);
const {cachedValue} = await contract.readState();
//const {cachedValue} = await contract.readState();
//logger.info("Result", await contract.getStorageValue('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'));
//console.dir(cachedValue.state);

File diff suppressed because it is too large Load Diff

View File

@@ -6442,6 +6442,11 @@ queue-microtask@^1.2.2, queue-microtask@^1.2.3:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
ramda@^0.29.0:
version "0.29.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.29.0.tgz#fbbb67a740a754c8a4cbb41e2a6e0eb8507f55fb"
integrity sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -7497,6 +7502,25 @@ warp-contracts-plugin-vrf@^1.0.3:
"@idena/vrf-js" "^1.0.1"
elliptic "^6.5.4"
warp-contracts@^1.4.5:
version "1.4.5"
resolved "https://registry.yarnpkg.com/warp-contracts/-/warp-contracts-1.4.5.tgz#a9113c1f4fe963ca6a65410ae0917e55db5cc7fe"
integrity sha512-hUlJt1DXQapPf7cQRUehv9bEMNl6gX9srJ3K3LFAjKvtv2evD22tdcAH0MYHb+QchGIBLqJwva9d2pC7pSA+PQ==
dependencies:
archiver "^5.3.0"
arweave "1.13.7"
async-mutex "^0.4.0"
bignumber.js "9.1.1"
events "3.3.0"
fast-copy "^3.0.0"
level "^8.0.0"
memory-level "^1.0.0"
safe-stable-stringify "2.4.1"
stream-buffers "^3.0.2"
unzipit "^1.4.0"
warp-isomorphic "1.0.4"
warp-wasm-metering "1.0.1"
warp-isomorphic@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/warp-isomorphic/-/warp-isomorphic-1.0.0.tgz#dccccfc046bc6ac77919f8be1024ced1385c70ea"