Skip to content

Commit 908930d

Browse files
authored
Merge pull request #2015 from aeternity/channel-reconnect
Fix reestablish flow in state channels
2 parents c8fbffe + 1f4a0c1 commit 908930d

File tree

8 files changed

+97
-92
lines changed

8 files changed

+97
-92
lines changed

src/channel/Base.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import EventEmitter from 'events';
22
import { w3cwebsocket as W3CWebSocket } from 'websocket';
33
import { snakeToPascal } from '../utils/string';
4-
import { buildTx, unpackTx } from '../tx/builder';
4+
import { unpackTx } from '../tx/builder';
55
import { Tag } from '../tx/builder/constants';
66
import * as handlers from './handlers';
77
import {
@@ -20,7 +20,7 @@ import {
2020
ChannelMessage,
2121
ChannelEvents,
2222
} from './internal';
23-
import { ChannelError } from '../utils/errors';
23+
import { ChannelError, IllegalArgumentError } from '../utils/errors';
2424
import { Encoded } from '../utils/encoder';
2525
import { TxUnpacked } from '../tx/builder/schema.generated';
2626
import { EntryTag } from '../tx/builder/entry/constants';
@@ -108,9 +108,16 @@ export default class Channel {
108108
}
109109

110110
static async _initialize<T extends Channel>(channel: T, options: ChannelOptions): Promise<T> {
111+
const reconnect = (options.existingFsmId ?? options.existingChannelId) != null;
112+
if (reconnect && (options.existingFsmId == null || options.existingChannelId == null)) {
113+
throw new IllegalArgumentError('`existingChannelId`, `existingFsmId` should be both provided or missed');
114+
}
115+
const reconnectHandler = handlers[
116+
options.reestablish === true ? 'awaitingReestablish' : 'awaitingReconnection'
117+
];
111118
await initialize(
112119
channel,
113-
options.existingFsmId != null ? handlers.awaitingReconnection : handlers.awaitingConnection,
120+
reconnect ? reconnectHandler : handlers.awaitingConnection,
114121
handlers.channelOpen,
115122
options,
116123
);
@@ -249,8 +256,7 @@ export default class Channel {
249256
* signed state and then terminates.
250257
*
251258
* The channel can be reestablished by instantiating another Channel instance
252-
* with two extra params: existingChannelId and offchainTx (returned from leave
253-
* method as channelId and signedTx respectively).
259+
* with two extra params: existingChannelId and existingFsmId.
254260
*
255261
* @example
256262
* ```js
@@ -290,16 +296,4 @@ export default class Channel {
290296
};
291297
});
292298
}
293-
294-
static async reconnect(options: ChannelOptions, txParams: any): Promise<Channel> {
295-
const { sign } = options;
296-
297-
return Channel.initialize({
298-
...options,
299-
reconnectTx: await sign(
300-
'reconnect',
301-
buildTx({ ...txParams, tag: Tag.ChannelClientReconnectTx }),
302-
),
303-
});
304-
}
305299
}

src/channel/handlers.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,40 @@ export function awaitingConnection(
143143
}
144144
}
145145

146+
export async function awaitingReestablish(
147+
channel: Channel,
148+
message: ChannelMessage,
149+
state: ChannelState,
150+
): Promise<ChannelFsm> {
151+
if (message.method === 'channels.info' && message.params.data.event === 'fsm_up') {
152+
channel._fsmId = message.params.data.fsm_id;
153+
return {
154+
handler: function awaitingChannelReestablished(
155+
_: Channel,
156+
message2: ChannelMessage,
157+
state2: ChannelState,
158+
): ChannelFsm | undefined {
159+
if (
160+
message2.method === 'channels.info'
161+
&& message2.params.data.event === 'channel_reestablished'
162+
) return { handler: awaitingOpenConfirmation };
163+
return handleUnexpectedMessage(channel, message2, state2);
164+
},
165+
};
166+
}
167+
return handleUnexpectedMessage(channel, message, state);
168+
}
169+
146170
export async function awaitingReconnection(
147171
channel: Channel,
148172
message: ChannelMessage,
149173
state: ChannelState,
150174
): Promise<ChannelFsm> {
151-
if (message.method === 'channels.info') {
152-
if (message.params.data.event === 'fsm_up') {
153-
channel._fsmId = message.params.data.fsm_id;
154-
const { signedTx } = await channel.state();
155-
changeState(channel, signedTx == null ? '' : buildTx(signedTx));
156-
return { handler: channelOpen };
157-
}
175+
if (message.method === 'channels.info' && message.params.data.event === 'fsm_up') {
176+
channel._fsmId = message.params.data.fsm_id;
177+
const { signedTx } = await channel.state();
178+
changeState(channel, signedTx == null ? '' : buildTx(signedTx));
179+
return { handler: channelOpen };
158180
}
159181
return handleUnexpectedMessage(channel, message, state);
160182
}
@@ -220,18 +242,25 @@ export function awaitingOnChainTx(
220242
function awaitingOpenConfirmation(
221243
channel: Channel,
222244
message: ChannelMessage,
245+
state: ChannelState,
223246
): ChannelFsm | undefined {
224247
if (message.method === 'channels.info' && message.params.data.event === 'open') {
225248
channel._channelId = message.params.channel_id;
226249
return {
227-
handler(_: Channel, message2: ChannelMessage): ChannelFsm | undefined {
250+
handler: function awaitingChannelsUpdate(
251+
_: Channel,
252+
message2: ChannelMessage,
253+
state2: ChannelState,
254+
): ChannelFsm | undefined {
228255
if (message2.method === 'channels.update') {
229256
changeState(channel, message2.params.data.state);
230257
return { handler: channelOpen };
231258
}
259+
return handleUnexpectedMessage(channel, message2, state2);
232260
},
233261
};
234262
}
263+
return handleUnexpectedMessage(channel, message, state);
235264
}
236265

237266
export async function channelOpen(

src/channel/internal.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
ChannelError,
1515
} from '../utils/errors';
1616
import { encodeContractAddress } from '../utils/crypto';
17-
import { buildTx } from '../tx/builder';
1817
import { ensureError } from '../utils/other';
1918

2019
export interface ChannelEvents {
@@ -53,7 +52,6 @@ export type SignTx = (tx: Encoded.Transaction, options?: SignOptions) => (
5352
* @see {@link https://github.com/aeternity/protocol/blob/6734de2e4c7cce7e5e626caa8305fb535785131d/node/api/channels_api_usage.md#channel-establishing-parameters}
5453
*/
5554
interface CommonChannelOptions {
56-
existingFsmId?: Encoded.Bytearray;
5755
/**
5856
* Channel url (for example: "ws://localhost:3001")
5957
*/
@@ -118,10 +116,14 @@ interface CommonChannelOptions {
118116
*/
119117
existingChannelId?: Encoded.Channel;
120118
/**
121-
* Offchain transaction (required if reestablishing a channel)
119+
* Existing FSM id (required if reestablishing a channel)
120+
*/
121+
existingFsmId?: Encoded.Bytearray;
122+
/**
123+
* Needs to be provided if reconnecting with calling `leave` before
122124
*/
123-
offChainTx?: Encoded.Transaction;
124-
reconnectTx?: Encoded.Transaction;
125+
// TODO: remove after solving https://github.com/aeternity/aeternity/issues/4399
126+
reestablish?: boolean;
125127
/**
126128
* The time waiting for a new event to be initiated (default: 600000)
127129
*/
@@ -174,7 +176,6 @@ interface CommonChannelOptions {
174176
* Function which verifies and signs transactions
175177
*/
176178
sign: SignTxWithTag;
177-
offchainTx?: Encoded.Transaction;
178179
}
179180

180181
export type ChannelOptions = CommonChannelOptions & ({
@@ -439,16 +440,7 @@ export async function initialize(
439440
onopen: async (event: Event) => {
440441
resolve();
441442
changeStatus(channel, 'connected', event);
442-
if (channelOptions.reconnectTx != null) {
443-
enterState(channel, { handler: openHandler });
444-
const { signedTx } = await channel.state();
445-
if (signedTx == null) {
446-
throw new ChannelError('`signedTx` missed in state while reconnection');
447-
}
448-
changeState(channel, buildTx(signedTx));
449-
} else {
450-
enterState(channel, { handler: connectionHandler });
451-
}
443+
enterState(channel, { handler: connectionHandler });
452444
ping(channel);
453445
},
454446
onclose: (event: ICloseEvent) => {

src/tx/builder/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ export enum Tag {
105105
ContractCreateTx = 42,
106106
ContractCallTx = 43,
107107
ChannelCreateTx = 50,
108-
// ChannelSetDelegatesTx = 501,
109108
ChannelDepositTx = 51,
110109
ChannelWithdrawTx = 52,
111110
ChannelForceProgressTx = 521,
@@ -114,7 +113,6 @@ export enum Tag {
114113
ChannelSlashTx = 55,
115114
ChannelSettleTx = 56,
116115
ChannelOffChainTx = 57,
117-
ChannelClientReconnectTx = 575,
118116
ChannelSnapshotSoloTx = 59,
119117
GaAttachTx = 80,
120118
GaMetaTx = 81,

src/tx/builder/schema.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -349,13 +349,6 @@ export const txSchema = [{
349349
ttl,
350350
fee,
351351
nonce: nonce('fromId'),
352-
}, {
353-
tag: shortUIntConst(Tag.ChannelClientReconnectTx),
354-
version: shortUIntConst(1, true),
355-
channelId: address(Encoding.Channel),
356-
round: shortUInt,
357-
role: string,
358-
pubkey: address(Encoding.AccountAddress),
359352
}, {
360353
tag: shortUIntConst(Tag.GaAttachTx),
361354
version: shortUIntConst(1, true),

test/integration/channel-other.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('Channel other', () => {
7575
const [initiatorBalanceBeforeClose, responderBalanceBeforeClose] = await getBalances();
7676
const closeSoloTx = await aeSdk.buildTx({
7777
tag: Tag.ChannelCloseSoloTx,
78-
channelId: await initiatorCh.id(),
78+
channelId: initiatorCh.id(),
7979
fromId: initiator.address,
8080
poi,
8181
payload: signedTx,
@@ -85,7 +85,7 @@ describe('Channel other', () => {
8585

8686
const settleTx = await aeSdk.buildTx({
8787
tag: Tag.ChannelSettleTx,
88-
channelId: await initiatorCh.id(),
88+
channelId: initiatorCh.id(),
8989
fromId: initiator.address,
9090
initiatorAmountFinal: balances[initiator.address],
9191
responderAmountFinal: balances[responder.address],
@@ -164,36 +164,33 @@ describe('Channel other', () => {
164164
.should.be.equal(true);
165165
}).timeout(timeoutBlock);
166166

167-
// https://github.com/aeternity/protocol/blob/d634e7a3f3110657900759b183d0734e61e5803a/node/api/channels_api_usage.md#reestablish
168-
it('can reconnect', async () => {
169-
expect(await initiatorCh.round()).to.be.equal(1);
170-
const result = await initiatorCh.update(
171-
initiator.address,
172-
responder.address,
173-
100,
174-
initiatorSign,
175-
);
176-
expect(result.accepted).to.equal(true);
177-
const channelId = await initiatorCh.id();
167+
it('can reconnect a channel without leave', async () => {
168+
expect(initiatorCh.round()).to.be.equal(1);
169+
await initiatorCh.update(initiator.address, responder.address, 100, initiatorSign);
170+
expect(initiatorCh.round()).to.be.equal(2);
171+
const channelId = initiatorCh.id();
178172
const fsmId = initiatorCh.fsmId();
179173
initiatorCh.disconnect();
174+
await waitForChannel(initiatorCh, ['disconnected']);
180175
const ch = await Channel.initialize({
181176
...sharedParams,
182177
...initiatorParams,
183178
existingChannelId: channelId,
184179
existingFsmId: fsmId,
185180
});
186-
await waitForChannel(ch);
181+
await waitForChannel(ch, ['open']);
187182
expect(ch.fsmId()).to.be.equal(fsmId);
188-
expect(await ch.round()).to.be.equal(2);
183+
expect(ch.round()).to.be.equal(2);
189184
const state = await ch.state();
190-
ch.disconnect();
191185
assertNotNull(state.signedTx);
192186
expect(state.signedTx.encodedTx.tag).to.be.equal(Tag.ChannelOffChainTx);
187+
await ch.update(initiator.address, responder.address, 100, initiatorSign);
188+
expect(ch.round()).to.be.equal(3);
189+
ch.disconnect();
193190
});
194191

195192
it('can post backchannel update', async () => {
196-
expect(await responderCh.round()).to.be.equal(1);
193+
expect(responderCh.round()).to.be.equal(1);
197194
initiatorCh.disconnect();
198195
const { accepted } = await responderCh.update(
199196
initiator.address,
@@ -202,7 +199,7 @@ describe('Channel other', () => {
202199
responderSign,
203200
);
204201
expect(accepted).to.equal(false);
205-
expect(await responderCh.round()).to.be.equal(1);
202+
expect(responderCh.round()).to.be.equal(1);
206203
const result = await responderCh.update(
207204
initiator.address,
208205
responder.address,
@@ -212,7 +209,7 @@ describe('Channel other', () => {
212209
),
213210
);
214211
result.accepted.should.equal(true);
215-
expect(await responderCh.round()).to.be.equal(2);
212+
expect(responderCh.round()).to.be.equal(2);
216213
expect(result.signedTx).to.be.a('string');
217214
});
218215
});

test/integration/channel-utils.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ import {
44
} from '../../src';
55
import { ChannelOptions, SignTxWithTag } from '../../src/channel/internal';
66

7-
export async function waitForChannel(channel: Channel): Promise<void> {
7+
export async function waitForChannel(channel: Channel, statuses: string[]): Promise<void> {
88
return new Promise((resolve, reject) => {
9-
channel.on('statusChanged', (status: string) => {
10-
switch (status) {
11-
case 'open':
12-
resolve();
13-
break;
14-
case 'disconnected':
15-
reject(new Error('Unexpected SC status: disconnected'));
16-
break;
17-
default:
9+
function handler(status: string): void {
10+
const expectedStatus = statuses.shift();
11+
if (status !== expectedStatus) {
12+
reject(new Error(`Expected SC status ${expectedStatus}, got ${status} instead`));
13+
channel.off('statusChanged', handler);
14+
} else if (statuses.length === 0) {
15+
resolve();
16+
channel.off('statusChanged', handler);
1817
}
19-
});
18+
}
19+
channel.on('statusChanged', handler);
2020
});
2121
}
2222

@@ -46,7 +46,10 @@ export async function initializeChannels(
4646
...sharedParams,
4747
...responderParams,
4848
});
49-
await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]);
49+
await Promise.all([
50+
waitForChannel(initiatorCh, ['accepted', 'signed', 'open']),
51+
waitForChannel(responderCh, ['halfSigned', 'open']),
52+
]);
5053
return [initiatorCh, responderCh];
5154
}
5255

test/integration/channel.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -572,33 +572,32 @@ describe('Channel', () => {
572572
// TODO: check `initiatorAmountFinal` and `responderAmountFinal`
573573
});
574574

575-
let existingChannelId: Encoded.Channel;
576-
let offchainTx: Encoded.Transaction;
577575
it('can leave a channel', async () => {
578576
initiatorCh.disconnect();
579577
responderCh.disconnect();
580578
[initiatorCh, responderCh] = await initializeChannels(initiatorParams, responderParams);
581-
initiatorCh.round(); // existingChannelRound
579+
await initiatorCh.update(initiator.address, responder.address, 100, initiatorSign);
582580
const result = await initiatorCh.leave();
583581
expect(result.channelId).to.satisfy((t: string) => t.startsWith('ch_'));
584582
expect(result.signedTx).to.satisfy((t: string) => t.startsWith('tx_'));
585-
existingChannelId = result.channelId;
586-
offchainTx = result.signedTx;
587583
});
588584

585+
// https://github.com/aeternity/protocol/blob/d634e7a3f3110657900759b183d0734e61e5803a/node/api/channels_api_usage.md#reestablish
589586
it('can reestablish a channel', async () => {
587+
expect(initiatorCh.round()).to.be.equal(2);
590588
initiatorCh = await Channel.initialize({
591589
...sharedParams,
592590
...initiatorParams,
593-
// @ts-expect-error TODO: use existingChannelId instead existingFsmId
594-
existingFsmId: existingChannelId,
595-
offchainTx,
591+
reestablish: true,
592+
existingChannelId: initiatorCh.id(),
593+
existingFsmId: initiatorCh.fsmId(),
596594
});
597-
await waitForChannel(initiatorCh);
598-
// TODO: why node doesn't return signed_tx when channel is reestablished?
599-
// initiatorCh.round().should.equal(existingChannelRound)
595+
await waitForChannel(initiatorCh, ['open']);
596+
expect(initiatorCh.round()).to.be.equal(2);
600597
sinon.assert.notCalled(initiatorSignTag);
601598
sinon.assert.notCalled(responderSignTag);
599+
await initiatorCh.update(initiator.address, responder.address, 100, initiatorSign);
600+
expect(initiatorCh.round()).to.be.equal(3);
602601
});
603602

604603
describe('throws errors', () => {

0 commit comments

Comments
 (0)