Skip to content

Commit

Permalink
Transaction confirmation in Warp gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
szynwelski committed Nov 21, 2023
1 parent 44d414e commit 4b6cc77
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 141 deletions.
123 changes: 78 additions & 45 deletions src/__tests__/integration/decentralized-sequencer/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import fs from 'fs';
import path from 'path';
import { createServer, Server } from 'http';
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;
Expand All @@ -26,28 +28,21 @@ describe('Testing sending of interactions to a decentralized sequencer', () => {
let arlocal: ArLocal;
let warp: Warp;
let contract: Contract<ExampleContractState>;
let sequencerServer: Server;
let centralizedSeqeuencerUrl: string;
let mockGwServer: Server;
let mockGwUrl: string;
let centralizedSequencerType: boolean;

beforeAll(async () => {
const port = 1813;
arlocal = new ArLocal(port, false);
await arlocal.start();

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

// a mock server simulating a centralized sequencer
centralizedSequencerType = false;
sequencerServer = createServer((req, res) => {
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 ? centralizedSeqeuencerUrl : DECENTRALIZED_SEQUENCER_URL,
url: centralizedSequencerType ? mockGwUrl : DECENTRALIZED_SEQUENCER_URL,
type: centralizedSequencerType ? 'centralized' : 'decentralized'
}));
return;
Expand All @@ -56,16 +51,57 @@ describe('Testing sending of interactions to a decentralized sequencer', () => {
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;
}
throw new Error("Unexpected sequencer path: " + req.url);
})

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 => {
sequencerServer.listen(() => {
const address = sequencerServer.address() as AddressInfo
centralizedSeqeuencerUrl = `http://localhost:${address.port}`
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,
Expand All @@ -77,6 +113,7 @@ describe('Testing sending of interactions to a decentralized sequencer', () => {
.custom(arweave, cacheOptions, 'custom')
.useWarpGateway(gatewayOptions, cacheOptions)
.build()
.useGwUrl(mockGwUrl)
.use(new DeployPlugin());

({ jwk: wallet } = await warp.generateWallet());
Expand All @@ -91,7 +128,7 @@ describe('Testing sending of interactions to a decentralized sequencer', () => {
});

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

Expand All @@ -100,7 +137,7 @@ describe('Testing sending of interactions to a decentralized sequencer', () => {
afterAll(async () => {
await arlocal.stop();
await new Promise(resolve => {
sequencerServer.close(resolve)
mockGwServer.close(resolve)
})
});

Expand All @@ -110,41 +147,37 @@ describe('Testing sending of interactions to a decentralized sequencer', () => {
if (tag.name === WARP_TAGS.SEQUENCER_NONCE) {
return Number(tag.value)
}
}
}
}
return -1
}

it('should add new interactions waiting for confirmation from the sequencer', async () => {
contract.setEvaluationOptions({ waitForConfirmation: true })
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)
expect(result?.bundlrResponse).toBeUndefined();
expect(result?.sequencerTxHash).toBeDefined();
});

it('should add new interactions without waiting for confirmation from the sequencer', async () => {
contract.setEvaluationOptions({ waitForConfirmation: false })
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)
expect(result?.bundlrResponse).toBeUndefined();
expect(result?.sequencerTxHash).toBeUndefined();
});

it('should follow the redirection returned by the centralized sequencer.', async () => {
centralizedSequencerType = true;
contract.setEvaluationOptions({
sequencerUrl: centralizedSeqeuencerUrl,
waitForConfirmation: true
});
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(4)
expect(result?.bundlrResponse).toBeUndefined();
expect(result?.sequencerTxHash).toBeDefined();
expect(getNonceFromResult(result)).toEqual(5)
});
});
38 changes: 16 additions & 22 deletions src/__tests__/integration/decentralized-sequencer/send-data-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ 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', () => {
let client: DecentralizedSequencerClient;

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

const createSignature = async (): Promise<Signature> => {
const wallet = await Arweave.crypto.generateJWK();
Expand All @@ -42,7 +43,8 @@ describe('Testing a decentralized sequencer client', () => {
}

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

Expand All @@ -51,7 +53,8 @@ describe('Testing a decentralized sequencer client', () => {
});

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

expect(client.sendDataItem(dataItem, false))
Expand All @@ -60,7 +63,8 @@ describe('Testing a decentralized sequencer client', () => {
});

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

expect(client.sendDataItem(dataItem, true))
Expand All @@ -69,7 +73,8 @@ describe('Testing a decentralized sequencer client', () => {
});

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

expect(client.sendDataItem(dataItem, true))
Expand All @@ -78,33 +83,22 @@ describe('Testing a decentralized sequencer client', () => {
});

it('should reject an unsigned data item', async () => {
const signature = await createSignature()
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 a confirmed result', async () => {
const signature = await createSignature();
const nonce = await client.getNonce(signature);
const dataItem = await createDataItem(signature, nonce);
const result = await client.sendDataItem(dataItem, true);

expect(result.sequencerMoved).toEqual(false);
expect(result.bundlrResponse).toBeUndefined();
expect(result.sequencerTxHash).toBeDefined();
});

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);
expect(result.bundlrResponse).toBeUndefined();
expect(result.sequencerTxHash).toBeUndefined();
});
});
4 changes: 2 additions & 2 deletions src/__tests__/unit/evaluation-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Evaluation options evaluator', () => {
mineArLocalBlocks: true,
remoteStateSyncEnabled: false,
remoteStateSyncSource: 'https://dre-1.warp.cc/contract',
sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/',
sequencerUrl: undefined,
sourceType: SourceType.BOTH,
stackTrace: {
saveState: false
Expand All @@ -30,7 +30,7 @@ describe('Evaluation options evaluator', () => {
unsafeClient: 'throw',
updateCacheForEachInteraction: false,
useKVStorage: false,
waitForConfirmation: false,
waitForConfirmation: undefined,
useConstructor: false,
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/',
whitelistSources: []
Expand Down
2 changes: 0 additions & 2 deletions src/contract/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ export interface BundlrResponse {
}

export interface WriteInteractionResponse {
bundlrResponse?: BundlrResponse;
originalTxId: string;
interactionTx: Transaction | DataItem;
sequencerTxHash?: string;
}

export interface DREContractStatusResponse<State> {
Expand Down
16 changes: 8 additions & 8 deletions src/contract/HandlerBasedContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,14 +341,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
);

const sequencerClient = await this.getSequencerClient();
const confirmInteraction = this._evaluationOptions.waitForConfirmation;
const sendResponse = await sequencerClient.sendDataItem(
interactionDataItem,
this._evaluationOptions.waitForConfirmation
confirmInteraction === undefined || confirmInteraction === true // by default, we wait for confirmation
);
if (sendResponse.sequencerMoved) {
this.logger.info(
`The sequencer at the given address (${this._evaluationOptions.sequencerUrl}) is redirecting to a new sequencer`
);
this.logger.info(`The sequencer is redirecting to a new sequencer`);
if (sequencerRedirected) {
throw new Error('Too many sequencer redirects');
}
Expand All @@ -357,16 +356,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
}

return {
bundlrResponse: sendResponse.bundlrResponse,
originalTxId: await interactionDataItem.id,
interactionTx: interactionDataItem,
sequencerTxHash: sendResponse.sequencerTxHash
interactionTx: interactionDataItem
};
}

private async getSequencerClient(): Promise<SequencerClient> {
if (!this._sequencerClient) {
this._sequencerClient = await createSequencerClient(this._evaluationOptions.sequencerUrl, this._warpFetchWrapper);
this._sequencerClient = await createSequencerClient(this.warp.gwUrl(), this._warpFetchWrapper);
}
return this._sequencerClient;
}
Expand Down Expand Up @@ -495,6 +492,9 @@ export class HandlerBasedContract<State> implements Contract<State> {

connect(signature: ArWallet | CustomSignature | Signer): Contract<State> {
this._signature = new Signature(this.warp, signature);
if (this._sequencerClient) {
this._sequencerClient.clearNonce();
}
return this;
}

Expand Down
1 change: 0 additions & 1 deletion src/contract/Signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export class Signature {
private readonly signatureProviderType: 'CustomSignature' | 'ArWallet' | 'BundlerSigner';
private readonly wallet;
private cachedAddress?: string;
sequencerNonce: number;

constructor(warp: Warp, walletOrSignature: SignatureProvider) {
this.warp = warp;
Expand Down
Loading

0 comments on commit 4b6cc77

Please sign in to comment.