Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client for sending interactions to the sequencer #472

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"test:integration:basic": "jest ./src/__tests__/integration/basic",
"test:integration:basic:load": "jest --silent=false --detectOpenHandles ./src/__tests__/integration/basic/contract-loading.test.ts ",
"test:integration:basic:arweave": "jest ./src/__tests__/integration/basic/arweave-transactions-loading",
"test:integration:decentralized-sequencer": "jest ./src/__tests__/integration/decentralized-sequencer --detectOpenHandles",
"test:integration:internal-writes": "jest ./src/__tests__/integration/internal-writes",
"test:integration:wasm": "jest ./src/__tests__/integration/wasm",
"test:regression": "node ./node_modules/.bin/jest ./src/__tests__/regression",
Expand Down
183 changes: 183 additions & 0 deletions src/__tests__/integration/decentralized-sequencer/interactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import fs from 'fs';
import path from 'path';
import { createServer, request, Server } from 'http';
import { DeployPlugin, ArweaveSigner } from 'warp-contracts-plugin-deploy';
import { Contract, WriteInteractionResponse } from '../../../contract/Contract';
import { Warp } from '../../../core/Warp';
import { WarpFactory, defaultCacheOptions, defaultWarpGwOptions } from '../../../core/WarpFactory';
import { SourceType } from '../../../core/modules/impl/WarpGatewayInteractionsLoader';
import { AddressInfo } from 'net';
import { WARP_TAGS } from '../../../core/KnownTags';
import { LoggerFactory } from '../../../logging/LoggerFactory';

interface ExampleContractState {
counter: number;
}

// FIXME: change to the address of the sequencer on dev
const DECENTRALIZED_SEQUENCER_URL = 'http://sequencer-0.warp.cc:1317';
const GW_URL = 'http://34.141.17.15:5666/';

describe('Testing sending of interactions to a decentralized sequencer', () => {
let contractSrc: string;
let initialState: string;
let wallet: JWKInterface;
let arlocal: ArLocal;
let warp: Warp;
let contract: Contract<ExampleContractState>;
let mockGwServer: Server;
let mockGwUrl: string;
let centralizedSequencerType: boolean;
let confirmAnyTx: boolean;

/**
* For testing purposes, operations returning the sequencer's address and registering/confirming interactions are mocked.
* Other requests are forwarded to the real Gateway.
*/
const mockGw = async () => {
mockGwServer = createServer((req, res) => {
if (req.url === '/gateway/sequencer/address') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
url: centralizedSequencerType ? mockGwUrl : DECENTRALIZED_SEQUENCER_URL,
type: centralizedSequencerType ? 'centralized' : 'decentralized'
}));
return;
} else if (req.url === '/gateway/v2/sequencer/register') {
centralizedSequencerType = false;
res.writeHead(301, { Location: DECENTRALIZED_SEQUENCER_URL });
res.end();
return;
} else if (req.url?.startsWith('/gateway/interactions/')) {
res.writeHead(confirmAnyTx ? 200 : 204);
res.end();
return;
}

var options = {
hostname: new URL(GW_URL).hostname,
port: new URL(GW_URL).port,
path: req.url,
method: req.method,
headers: req.headers
};

var proxy = request(options, (gwRes) => {
if (gwRes.statusCode) {
res.writeHead(gwRes.statusCode, gwRes.headers)
gwRes.pipe(res, {
end: true
});
}
});

req.pipe(proxy, {
end: true
});
});
await new Promise<void>(resolve => {
mockGwServer.listen(() => {
const address = mockGwServer.address() as AddressInfo
mockGwUrl = `http://localhost:${address.port}`
resolve()
})
});
}

beforeAll(async () => {
LoggerFactory.INST.logLevel('debug');
const port = 1813;
arlocal = new ArLocal(port, false);
await arlocal.start();

const arweave = Arweave.init({
host: 'localhost',
port: port,
protocol: 'http'
});

centralizedSequencerType = false;
confirmAnyTx = false;
await mockGw();

const cacheOptions = {
...defaultCacheOptions,
inMemory: true
}
const gatewayOptions = { ...defaultWarpGwOptions, source: SourceType.WARP_SEQUENCER, confirmationStatus: { notCorrupted: true } }

warp = WarpFactory
.custom(arweave, cacheOptions, 'custom')
.useWarpGateway(gatewayOptions, cacheOptions)
.build()
.useGwUrl(mockGwUrl)
.use(new DeployPlugin());

({ jwk: wallet } = await warp.generateWallet());

contractSrc = fs.readFileSync(path.join(__dirname, '../data/example-contract.js'), 'utf8');
initialState = fs.readFileSync(path.join(__dirname, '../data/example-contract-state.json'), 'utf8');

const { contractTxId } = await warp.deploy({
wallet: new ArweaveSigner(wallet),
initState: initialState,
src: contractSrc
});

contract = warp.contract<ExampleContractState>(contractTxId).setEvaluationOptions({
sequencerUrl: mockGwUrl
});
contract.connect(wallet);

});

afterAll(async () => {
await arlocal.stop();
await new Promise(resolve => {
mockGwServer.close(resolve)
})
});

const getNonceFromResult = (result: WriteInteractionResponse | null): number => {
if (result) {
for (let tag of result.interactionTx.tags) {
if (tag.name === WARP_TAGS.SEQUENCER_NONCE) {
return Number(tag.value)
}
}
}
return -1
}

it('should follow the redirection returned by the centralized sequencer.', async () => {
confirmAnyTx = true;
centralizedSequencerType = true;
contract.setEvaluationOptions({
waitForConfirmation: true
});

await contract.writeInteraction({ function: 'add' });
const result = await contract.writeInteraction({ function: 'add' });
expect(getNonceFromResult(result)).toEqual(1)
});

it('should add new interactions waiting for confirmation from the gateway', async () => {
contract.setEvaluationOptions({ waitForConfirmation: true })
setTimeout(() => confirmAnyTx = true, 2000);

await contract.writeInteraction({ function: 'add' });
const result = await contract.writeInteraction({ function: 'add' });
expect(getNonceFromResult(result)).toEqual(3)
});

it('should add new interactions without waiting for confirmation from the gateway', async () => {
contract.setEvaluationOptions({ waitForConfirmation: false })

await contract.writeInteraction({ function: 'add' });
const result = await contract.writeInteraction({ function: 'add' });
expect(getNonceFromResult(result)).toEqual(5)
});
});
104 changes: 104 additions & 0 deletions src/__tests__/integration/decentralized-sequencer/send-data-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Arweave from 'arweave';
import { createData, DataItem, Signer } from 'warp-arbundles';
import { ArweaveSigner } from 'warp-contracts-plugin-deploy';
import { DecentralizedSequencerClient } from '../../../contract/sequencer/DecentralizedSequencerClient';
import { SMART_WEAVE_TAGS, WARP_TAGS } from '../../../core/KnownTags';
import { Tag } from '../../../utils/types/arweave-types';
import { WarpFactory } from '../../../core/WarpFactory';
import { WarpFetchWrapper } from '../../../core/WarpFetchWrapper';
import { Signature } from '../../../contract/Signature';
import { SequencerClient } from '../../../contract/sequencer/SequencerClient';

// FIXME: change to the address of the sequencer on dev
const SEQUENCER_URL = 'http://sequencer-0.warp.cc:1317';
const GW_URL = 'http://34.141.17.15:5666/';

describe('Testing a decentralized sequencer client', () => {

const createClient = (): SequencerClient => {
const warpFetchWrapper = new WarpFetchWrapper(WarpFactory.forLocal())
return new DecentralizedSequencerClient(SEQUENCER_URL, GW_URL, warpFetchWrapper);
}

const createSignature = async (): Promise<Signature> => {
const wallet = await Arweave.crypto.generateJWK();
const signer = new ArweaveSigner(wallet);
return new Signature(WarpFactory.forLocal(), signer)
}

const createDataItem = async (signature: Signature, nonce: number, addNonceTag = true, addContractTag = true, signDataItem = true): Promise<DataItem> => {
const signer = signature.bundlerSigner;
const tags: Tag[] = [];
if (addNonceTag) {
tags.push(new Tag(WARP_TAGS.SEQUENCER_NONCE, String(nonce)));
}
if (addContractTag) {
tags.push(new Tag(SMART_WEAVE_TAGS.CONTRACT_TX_ID, "unit test contract"));
}
const dataItem = createData('some data', signer, { tags });
if (signDataItem) {
await dataItem.sign(signer);
}
return dataItem;
}

it('should return consecutive nonces for a given signature', async () => {
const client = createClient();
const signature = await createSignature();
let nonce = await client.getNonce(signature);
expect(nonce).toEqual(0);

nonce = await client.getNonce(signature);
expect(nonce).toEqual(1);
});

it('should reject a data item with an invalid nonce', async () => {
const client = createClient();
const signature = await createSignature();
const dataItem = await createDataItem(signature, 13);

expect(client.sendDataItem(dataItem, false))
.rejects
.toThrowError('account sequence mismatch, expected 0, got 13: incorrect account sequence');
});

it('should reject a data item without nonce', async () => {
const client = createClient();
const signature = await createSignature();
const dataItem = await createDataItem(signature, 0, false);

expect(client.sendDataItem(dataItem, true))
.rejects
.toThrowError('no sequencer nonce tag');
});

it('should reject a data item without contract', async () => {
const client = createClient();
const signature = await createSignature();
const dataItem = await createDataItem(signature, 0, true, false);

expect(client.sendDataItem(dataItem, true))
.rejects
.toThrowError('no contract tag');
});

it('should reject an unsigned data item', async () => {
const client = createClient();
const signature = await createSignature();
const dataItem = await createDataItem(signature, 0, true, true, false);

expect(client.sendDataItem(dataItem, true))
.rejects
.toThrowError('data item verification error');
});

it('should return an unconfirmed result', async () => {
const client = createClient();
const signature = await createSignature();
const nonce = await client.getNonce(signature);
const dataItem = await createDataItem(signature, nonce);
const result = await client.sendDataItem(dataItem, false);

expect(result.sequencerMoved).toEqual(false);
});
});
Loading