From 1f4a0c1d43d9f37938bade1f9684383df1da260a Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 4 Sep 2024 18:20:57 +1000 Subject: [PATCH] fix(channel): reestablish flow --- src/channel/Base.ts | 11 ++++++-- src/channel/handlers.ts | 45 +++++++++++++++++++++++++------ src/channel/internal.ts | 5 ++++ test/integration/channel-other.ts | 16 +++++------ test/integration/channel.ts | 19 +++++++------ 5 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/channel/Base.ts b/src/channel/Base.ts index c4fecd016e..be346ca7fe 100644 --- a/src/channel/Base.ts +++ b/src/channel/Base.ts @@ -20,7 +20,7 @@ import { ChannelMessage, ChannelEvents, } from './internal'; -import { ChannelError } from '../utils/errors'; +import { ChannelError, IllegalArgumentError } from '../utils/errors'; import { Encoded } from '../utils/encoder'; import { TxUnpacked } from '../tx/builder/schema.generated'; import { EntryTag } from '../tx/builder/entry/constants'; @@ -108,9 +108,16 @@ export default class Channel { } static async _initialize(channel: T, options: ChannelOptions): Promise { + const reconnect = (options.existingFsmId ?? options.existingChannelId) != null; + if (reconnect && (options.existingFsmId == null || options.existingChannelId == null)) { + throw new IllegalArgumentError('`existingChannelId`, `existingFsmId` should be both provided or missed'); + } + const reconnectHandler = handlers[ + options.reestablish === true ? 'awaitingReestablish' : 'awaitingReconnection' + ]; await initialize( channel, - options.existingFsmId != null ? handlers.awaitingReconnection : handlers.awaitingConnection, + reconnect ? reconnectHandler : handlers.awaitingConnection, handlers.channelOpen, options, ); diff --git a/src/channel/handlers.ts b/src/channel/handlers.ts index 614f16b945..c373efbc89 100644 --- a/src/channel/handlers.ts +++ b/src/channel/handlers.ts @@ -143,18 +143,40 @@ export function awaitingConnection( } } +export async function awaitingReestablish( + channel: Channel, + message: ChannelMessage, + state: ChannelState, +): Promise { + if (message.method === 'channels.info' && message.params.data.event === 'fsm_up') { + channel._fsmId = message.params.data.fsm_id; + return { + handler: function awaitingChannelReestablished( + _: Channel, + message2: ChannelMessage, + state2: ChannelState, + ): ChannelFsm | undefined { + if ( + message2.method === 'channels.info' + && message2.params.data.event === 'channel_reestablished' + ) return { handler: awaitingOpenConfirmation }; + return handleUnexpectedMessage(channel, message2, state2); + }, + }; + } + return handleUnexpectedMessage(channel, message, state); +} + export async function awaitingReconnection( channel: Channel, message: ChannelMessage, state: ChannelState, ): Promise { - if (message.method === 'channels.info') { - if (message.params.data.event === 'fsm_up') { - channel._fsmId = message.params.data.fsm_id; - const { signedTx } = await channel.state(); - changeState(channel, signedTx == null ? '' : buildTx(signedTx)); - return { handler: channelOpen }; - } + if (message.method === 'channels.info' && message.params.data.event === 'fsm_up') { + channel._fsmId = message.params.data.fsm_id; + const { signedTx } = await channel.state(); + changeState(channel, signedTx == null ? '' : buildTx(signedTx)); + return { handler: channelOpen }; } return handleUnexpectedMessage(channel, message, state); } @@ -220,18 +242,25 @@ export function awaitingOnChainTx( function awaitingOpenConfirmation( channel: Channel, message: ChannelMessage, + state: ChannelState, ): ChannelFsm | undefined { if (message.method === 'channels.info' && message.params.data.event === 'open') { channel._channelId = message.params.channel_id; return { - handler(_: Channel, message2: ChannelMessage): ChannelFsm | undefined { + handler: function awaitingChannelsUpdate( + _: Channel, + message2: ChannelMessage, + state2: ChannelState, + ): ChannelFsm | undefined { if (message2.method === 'channels.update') { changeState(channel, message2.params.data.state); return { handler: channelOpen }; } + return handleUnexpectedMessage(channel, message2, state2); }, }; } + return handleUnexpectedMessage(channel, message, state); } export async function channelOpen( diff --git a/src/channel/internal.ts b/src/channel/internal.ts index b82cb7ce92..f9afe1a5d8 100644 --- a/src/channel/internal.ts +++ b/src/channel/internal.ts @@ -119,6 +119,11 @@ interface CommonChannelOptions { * Existing FSM id (required if reestablishing a channel) */ existingFsmId?: Encoded.Bytearray; + /** + * Needs to be provided if reconnecting with calling `leave` before + */ + // TODO: remove after solving https://github.com/aeternity/aeternity/issues/4399 + reestablish?: boolean; /** * The time waiting for a new event to be initiated (default: 600000) */ diff --git a/test/integration/channel-other.ts b/test/integration/channel-other.ts index efdb3257ea..0fd30086c4 100644 --- a/test/integration/channel-other.ts +++ b/test/integration/channel-other.ts @@ -164,16 +164,10 @@ describe('Channel other', () => { .should.be.equal(true); }).timeout(timeoutBlock); - // https://github.com/aeternity/protocol/blob/d634e7a3f3110657900759b183d0734e61e5803a/node/api/channels_api_usage.md#reestablish - it('can reconnect', async () => { + it('can reconnect a channel without leave', async () => { expect(initiatorCh.round()).to.be.equal(1); - const result = await initiatorCh.update( - initiator.address, - responder.address, - 100, - initiatorSign, - ); - expect(result.accepted).to.equal(true); + await initiatorCh.update(initiator.address, responder.address, 100, initiatorSign); + expect(initiatorCh.round()).to.be.equal(2); const channelId = initiatorCh.id(); const fsmId = initiatorCh.fsmId(); initiatorCh.disconnect(); @@ -188,9 +182,11 @@ describe('Channel other', () => { expect(ch.fsmId()).to.be.equal(fsmId); expect(ch.round()).to.be.equal(2); const state = await ch.state(); - ch.disconnect(); assertNotNull(state.signedTx); expect(state.signedTx.encodedTx.tag).to.be.equal(Tag.ChannelOffChainTx); + await ch.update(initiator.address, responder.address, 100, initiatorSign); + expect(ch.round()).to.be.equal(3); + ch.disconnect(); }); it('can post backchannel update', async () => { diff --git a/test/integration/channel.ts b/test/integration/channel.ts index 5a26aafd7d..2e952c6ea6 100644 --- a/test/integration/channel.ts +++ b/test/integration/channel.ts @@ -572,33 +572,32 @@ describe('Channel', () => { // TODO: check `initiatorAmountFinal` and `responderAmountFinal` }); - let existingChannelId: Encoded.Channel; - let offchainTx: Encoded.Transaction; it('can leave a channel', async () => { initiatorCh.disconnect(); responderCh.disconnect(); [initiatorCh, responderCh] = await initializeChannels(initiatorParams, responderParams); - initiatorCh.round(); // existingChannelRound + await initiatorCh.update(initiator.address, responder.address, 100, initiatorSign); const result = await initiatorCh.leave(); expect(result.channelId).to.satisfy((t: string) => t.startsWith('ch_')); expect(result.signedTx).to.satisfy((t: string) => t.startsWith('tx_')); - existingChannelId = result.channelId; - offchainTx = result.signedTx; }); + // https://github.com/aeternity/protocol/blob/d634e7a3f3110657900759b183d0734e61e5803a/node/api/channels_api_usage.md#reestablish it('can reestablish a channel', async () => { + expect(initiatorCh.round()).to.be.equal(2); initiatorCh = await Channel.initialize({ ...sharedParams, ...initiatorParams, - // @ts-expect-error TODO: use existingChannelId instead existingFsmId - existingFsmId: existingChannelId, - offchainTx, + reestablish: true, + existingChannelId: initiatorCh.id(), + existingFsmId: initiatorCh.fsmId(), }); await waitForChannel(initiatorCh, ['open']); - // TODO: why node doesn't return signed_tx when channel is reestablished? - // initiatorCh.round().should.equal(existingChannelRound) + expect(initiatorCh.round()).to.be.equal(2); sinon.assert.notCalled(initiatorSignTag); sinon.assert.notCalled(responderSignTag); + await initiatorCh.update(initiator.address, responder.address, 100, initiatorSign); + expect(initiatorCh.round()).to.be.equal(3); }); describe('throws errors', () => {