Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #95 from ar-io/PE-5458-blocklist-contract-evaluation
Browse files Browse the repository at this point in the history
feat(PE-5458): blocklist contract evaluation
  • Loading branch information
dtfiedler authored Jan 18, 2024
2 parents 75dc467 + 6c9b721 commit 2f86adf
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 19 deletions.
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:
GATEWAY_PORT: ${GATEWAY_PORT:-1984}
GATEWAY_PROTOCOL: ${GATEWAY_PROTOCOL:-http}
PREFETCH_CONTRACTS: ${PREFETCH_CONTRACTS:-false}
BLOCKLISTED_CONTRACTS: ${BLOCKLISTED_CONTRACTS:-fbU8Y4NMKKzP4rmAYeYj6tDrVDo9XNbdyq5IZPA31WQ}
ports:
- '3000:3000'

Expand All @@ -33,6 +34,7 @@ services:
GATEWAY_HOST: ${GATEWAY_HOST:-arlocal}
GATEWAY_PORT: ${GATEWAY_PORT:-1984}
GATEWAY_PROTOCOL: ${GATEWAY_PROTOCOL:-http}
BLOCKLISTED_CONTRACTS: ${BLOCKLISTED_CONTRACTS:-fbU8Y4NMKKzP4rmAYeYj6tDrVDo9XNbdyq5IZPA31WQ}
depends_on:
- arlocal
- arns-service
5 changes: 4 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { EvaluationOptions } from 'warp-contracts';

export const PREFETCH_CONTRACTS = process.env.PREFETCH_CONTRACTS === 'true';
export const BOOTSTRAP_CACHE = process.env.BOOTSTRAP_CACHE === 'true';
export const ARNS_CONTRACT_ID_REGEX = '([a-zA-Z0-9-_s+]{43})';
export const BLOCKLISTED_CONTRACTS = process.env.BLOCKLISTED_CONTRACTS
? process.env.BLOCKLISTED_CONTRACTS.split(',')
: ['fbU8Y4NMKKzP4rmAYeYj6tDrVDo9XNbdyq5IZPA31WQ'];
export const ARWEAVE_TX_ID_REGEX = '([a-zA-Z0-9-_s+]{43})';
export const ARNS_NAME_REGEX = '([a-zA-Z0-9-s+]{1,51})';
export const SUB_CONTRACT_EVALUATION_TIMEOUT_MS = 10_000; // 10 sec state timeout - non configurable
export const DEFAULT_REQUEST_TIMEOUT_MS = 180_000;
Expand Down
6 changes: 6 additions & 0 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ export const mismatchedInteractionCount = new promClient.Counter({
name: 'mismatched_interactions_count',
help: 'An interaction found via GQL was not evaluated by warp for a contract',
});

export const blockListedContractCount = new promClient.Counter({
name: 'blocklisted_contract_count',
help: 'A contract was blocklisted',
labelNames: ['contractTxId'],
});
40 changes: 40 additions & 0 deletions src/middleware/blocklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Next } from 'koa';
import { BLOCKLISTED_CONTRACTS } from '../constants';
import { KoaContext } from '../types';
import logger from '../logger';
import { blockListedContractCount } from '../metrics';

export async function blocklistMiddleware(ctx: KoaContext, next: Next) {
const { contractTxId } = ctx.params;
if (BLOCKLISTED_CONTRACTS.includes(contractTxId)) {
blockListedContractCount
.labels({
contractTxId,
})
.inc();
logger.debug('Blocking contract evaluation', {
contractTxId,
});
ctx.status = 403;
ctx.message = 'Contract is blocklisted.';
return;
}

return next();
}
41 changes: 27 additions & 14 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Router from '@koa/router';
import { ARNS_CONTRACT_ID_REGEX, ARNS_NAME_REGEX } from './constants';
import { ARWEAVE_TX_ID_REGEX, ARNS_NAME_REGEX } from './constants';
import {
contractBalanceHandler,
contractFieldHandler,
Expand All @@ -32,6 +32,7 @@ import { swaggerDocs } from './routes/swagger';
import { KoaContext } from './types';
import { getPrefetchStatusCode } from './system';
import { prefetchContractTxIds } from './config';
import { blocklistMiddleware } from './middleware/blocklist';

const router: Router = new Router();

Expand All @@ -46,33 +47,40 @@ router.get('/healthcheck', (ctx) => {

// V1 endpoints
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}`,
blocklistMiddleware,
contractHandler,
);
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/interactions`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/interactions`,
blocklistMiddleware,
contractInteractionsHandler,
);
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/interactions/:address${ARNS_CONTRACT_ID_REGEX}`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/interactions/:address${ARWEAVE_TX_ID_REGEX}`,
blocklistMiddleware,
contractInteractionsHandler,
);
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/records/:name${ARNS_NAME_REGEX}`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/records/:name${ARNS_NAME_REGEX}`,
blocklistMiddleware,
contractRecordHandler,
);
router.get(
// handles query params to filter records with a specific contractTxId (e.g. /v1/contract/<ARNS_REGISTRY>/records?contractTxId=<ARNS_CONTRACT_ID>)
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/records`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/records`,
blocklistMiddleware,
contractRecordFilterHandler,
);
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/balances/:address${ARNS_CONTRACT_ID_REGEX}`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/balances/:address${ARWEAVE_TX_ID_REGEX}`,
blocklistMiddleware,
contractBalanceHandler,
);
// RESTful API to easy get auction prices
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/auctions/:name${ARNS_NAME_REGEX}`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/auctions/:name${ARNS_NAME_REGEX}`,
blocklistMiddleware,
(ctx: KoaContext) => {
// set params for auction read interaction and then use our generic handler
ctx.params.functionName = 'auction';
Expand All @@ -84,7 +92,8 @@ router.get(
},
);
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/price`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/price`,
blocklistMiddleware,
(ctx: KoaContext) => {
// set params for auction read interaction and then use our generic handler
ctx.params.functionName = 'priceForInteraction';
Expand All @@ -93,24 +102,28 @@ router.get(
);
// generic handler that handles read APIs for any contract function
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/read/:functionName`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/read/:functionName`,
blocklistMiddleware,
contractReadInteractionHandler,
);
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/reserved/:name${ARNS_NAME_REGEX}`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/reserved/:name${ARNS_NAME_REGEX}`,
blocklistMiddleware,
contractReservedHandler,
);
// fallback for any other contract fields that don't include additional logic (i.e. this just returns partial contract state)
router.get(
`/v1/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}/:field`,
`/v1/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}/:field`,
blocklistMiddleware,
contractFieldHandler,
);
router.get(
`/v1/wallet/:address${ARNS_CONTRACT_ID_REGEX}/contracts`,
`/v1/wallet/:address${ARWEAVE_TX_ID_REGEX}/contracts`,
walletContractHandler,
);
router.get(
`/v1/wallet/:address${ARNS_CONTRACT_ID_REGEX}/contract/:contractTxId${ARNS_CONTRACT_ID_REGEX}`,
`/v1/wallet/:address${ARWEAVE_TX_ID_REGEX}/contract/:contractTxId${ARWEAVE_TX_ID_REGEX}`,
blocklistMiddleware,
contractInteractionsHandler,
);

Expand Down
23 changes: 19 additions & 4 deletions src/routes/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import {
} from '../api/graphql';
import { isValidContractType, validateStateAndOwnership } from '../api/warp';
import {
BLOCKLISTED_CONTRACTS,
DEFAULT_STATE_EVALUATION_TIMEOUT_MS,
allowedContractTypes,
} from '../constants';
import * as _ from 'lodash';
import { BadRequestError } from '../errors';
import { blockListedContractCount } from '../metrics';

export async function walletContractHandler(ctx: KoaContext) {
const { address } = ctx.params;
Expand Down Expand Up @@ -76,9 +78,22 @@ export async function walletContractHandler(ctx: KoaContext) {
const startTime = Date.now();
const validContractsOfType = (
await Promise.allSettled(
[...deployedOrOwned].map(async (id: string) =>
[...deployedOrOwned].map(async (id: string) => {
// do not evaluate any blocklisted contracts
if (BLOCKLISTED_CONTRACTS.includes(id)) {
logger.debug('Skipping blocklisted contract.', {
contractTxId: id,
});
blockListedContractCount
.labels({
contractTxId: id,
})
.inc();
return null;
}

// do not pass any evaluation options, the contract manifests will be fetched for each of these so they properly evaluate
(await validateStateAndOwnership({
return (await validateStateAndOwnership({
contractTxId: id,
warp,
type,
Expand All @@ -87,8 +102,8 @@ export async function walletContractHandler(ctx: KoaContext) {
signal: abortSignal,
}))
? id
: null,
),
: null;
}),
)
).map((i) => (i.status === 'fulfilled' ? i.value : null));
logger.info(`Finished evaluating contracts.`, {
Expand Down
49 changes: 49 additions & 0 deletions tests/integration/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ describe('Integration tests', () => {
describe('/v1', () => {
describe('/contract', () => {
describe('/:contractTxId', () => {
it('should not evaluate blocklisted contracts', async () => {
const blocklistedContractTxId = process.env.BLOCKLISTED_CONTRACTS;
const { status, data, statusText } = await axios.get(
`/v1/contract/${blocklistedContractTxId}`,
);
expect(status).to.equal(403);
expect(data).to.equal('Contract is blocklisted.');
expect(statusText).to.equal('Contract is blocklisted.');
});
it('should return the contract state and id and default evaluation options', async () => {
const { status, data } = await axios.get(`/v1/contract/${id}`);
expect(status).to.equal(200);
Expand Down Expand Up @@ -227,6 +236,16 @@ describe('Integration tests', () => {
});
});
describe('/:contractTxId/price', () => {
it('should not evaluate blocklisted contracts', async () => {
const blocklistedContractTxId = process.env.BLOCKLISTED_CONTRACTS;
const { status, data, statusText } = await axios.get(
`/v1/contract/${blocklistedContractTxId}/price`,
);
expect(status).to.equal(403);
expect(data).to.equal('Contract is blocklisted.');
expect(statusText).to.equal('Contract is blocklisted.');
});

it('should properly handle price interaction inputs', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/price?qty=100`,
Expand All @@ -242,6 +261,16 @@ describe('Integration tests', () => {
});

describe('/:contractTxId/interactions', () => {
it('should not evaluate blocklisted contracts', async () => {
const blocklistedContractTxId = process.env.BLOCKLISTED_CONTRACTS;
const { status, data, statusText } = await axios.get(
`/v1/contract/${blocklistedContractTxId}/interactions`,
);
expect(status).to.equal(403);
expect(data).to.equal('Contract is blocklisted.');
expect(statusText).to.equal('Contract is blocklisted.');
});

it('should return the contract interactions when no query params are provided', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions`,
Expand Down Expand Up @@ -388,6 +417,16 @@ describe('Integration tests', () => {
'auctions',
'reserved',
]) {
it('should not evaluate blocklisted contracts', async () => {
const blocklistedContractTxId = process.env.BLOCKLISTED_CONTRACTS;
const { status, data, statusText } = await axios.get(
`/v1/contract/${blocklistedContractTxId}/${field}`,
);
expect(status).to.equal(403);
expect(data).to.equal('Contract is blocklisted.');
expect(statusText).to.equal('Contract is blocklisted.');
});

it(`should return the correct state value for ${field}`, async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/${field}`,
Expand Down Expand Up @@ -653,6 +692,16 @@ describe('Integration tests', () => {
});

describe('/:address/contracts/:contractTxId', () => {
it('should not evaluate blocklisted contracts', async () => {
const blocklistedContractTxId = process.env.BLOCKLISTED_CONTRACTS;
const { status, data, statusText } = await axios.get(
`/v1/wallet/${walletAddress}/contract/${blocklistedContractTxId}`,
);
expect(status).to.equal(403);
expect(data).to.equal('Contract is blocklisted.');
expect(statusText).to.equal('Contract is blocklisted.');
});

it('should return the the first page wallets contract interactions by default, with default page size of 100', async () => {
const { status, data } = await axios.get(
`/v1/wallet/${walletAddress}/contract/${id}`,
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export async function mochaGlobalSetup() {
// set in the environment
process.env.DEPLOYED_REGISTRY_CONTRACT_TX_ID = contractTxId;
process.env.DEPLOYED_ANT_CONTRACT_TX_ID = antContractTxId;

// blocklisted contract
process.env.BLOCKLISTED_CONTRACTS =
process.env.BLOCKLISTED_CONTRACTS ||
'fbU8Y4NMKKzP4rmAYeYj6tDrVDo9XNbdyq5IZPA31WQ';
console.log(
`Successfully setup ArLocal and deployed contracts.\nRegistry: ${contractTxId}\nANT: ${antContractTxId}`,
);
Expand Down

0 comments on commit 2f86adf

Please sign in to comment.