Skip to content

fix(mls): ensure conversations are established using remote epoch state [WPB-24984]#21174

Open
thisisamir98 wants to merge 8 commits intodevfrom
WPB-24984-logs
Open

fix(mls): ensure conversations are established using remote epoch state [WPB-24984]#21174
thisisamir98 wants to merge 8 commits intodevfrom
WPB-24984-logs

Conversation

@thisisamir98
Copy link
Copy Markdown
Collaborator

@thisisamir98 thisisamir98 commented Apr 29, 2026

BugWPB-24984 [Web] "Couldnt find conversation" error

Refactor MLS conversation establishment to rely on core-crypto + backend epoch checks instead of passing a local epoch through call sites. This handles locally unestablished groups more safely (wipe + rehydrate/join), introduces safe epoch retrieval via getSafeEpoch, and adds targeted logging around init, decrypt failures, external commits, and pending proposal commits to improve recovery/debugging.

…te [WPB-24984]

Refactor MLS conversation establishment to rely on core-crypto + backend epoch checks instead of passing a local epoch through call sites. This handles locally unestablished groups more safely (wipe + rehydrate/join), introduces safe epoch retrieval via `getSafeEpoch`, and adds targeted logging around init, decrypt failures, external commits, and pending proposal commits to improve recovery/debugging.
e-maad
e-maad previously approved these changes Apr 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

🔗 Download Full Report Artifact

🧪 Playwright Test Summary

  • Passed: 15
  • Failed: 0
  • Skipped: 0
  • 🔁 Flaky: 0
  • 📊 Total: 15
  • Total Runtime: 83.0s (~ 1 min 23 sec)

typfel
typfel previously approved these changes Apr 30, 2026
@sonarqubecloud
Copy link
Copy Markdown

};

const mockSafeEpoch = (core: Core) => {
(core.service!.mls! as any).getSafeEpoch = jest
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • never use any type assertions
  • never use definite assignment assertions !

if (remoteEpoch === 0) {
this.logger.info('Establishing conversation as remote epoch is 0', {remoteEpoch});
await this.establishMlsGroupConversation({conversationId, groupId, epoch: remoteEpoch, core});
return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a missing/invalid remote epoch, now silently no-ops. The removed recovery code threw when the remote epoch was unavailable. The new implementation does nothing if remoteConversation.epoch is undefined, null or otherwise falsy but not exactly 0. In the message-send path, ensureConversationExists can return successfully and then sending continues.

const repositoryCore = (conversationRepository as any).core as Core;
jest.spyOn(repositoryCore.service!.conversation, 'mlsGroupExistsLocally').mockResolvedValue(false);
jest
.spyOn((conversationRepository as any).conversationService, 'getConversationById')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No as any please

return Promise.resolve(!groupId.includes('unestablished'));
});

(repositoryCore.service!.mls! as any).getSafeEpoch = jest.fn().mockResolvedValue({isOk: true, error: null});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never use as any please

});
}
const conversationExistsOnCoreCrypto = await this.conversationService.mlsGroupExistsLocally(groupId);
const coreCryptoEpochNumberResult = await core.service?.mls?.getSafeEpoch(groupId);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid optional chaining here and introduce an explicit type guard for the MLS service before calling getSafeEpoch? Right now core.service?.mls?.getSafeEpoch(groupId) can return undefined, but line 2234 treats the result as present.


if (coreCryptoEpochNumberResult.isErr) {
this.logger.warn(
'conversation existed on core crypto but there was an error when retrieving its epoch number',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log message is misleading because it says the conversation existed on core-crypto, but this branch only checks getSafeEpoch failure. It can run even when conversationExistsOnCoreCrypto is false.

);
}

if (conversationExistsOnCoreCrypto && coreCryptoEpochNumber > 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid comparing a possibly undefined value here? coreCryptoEpochNumber is undefined when the result is Err, so this condition depends on a value that is not cleanly narrowed.

await core.service?.conversation?.wipeMLSConversation(groupId);
}

const remoteConversation = await this.conversationService.getConversationById(conversationId);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an explicit type guard for remoteConversation.epoch right after fetching the remote conversation? This method’s behavior depends entirely on epoch being a valid number, but the current code accepts any shape and lets invalid values fall through.

const remoteEpoch = remoteConversation.epoch;

// establish the conversation if remote epoch is 0
if (remoteEpoch === 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the epoch decision exhaustive instead of relying on partial conditions? Or in other words: this handles 0 but there is no corresponding explicit branch for invalid epoch values.

// join by external commit
this.logger.info('Joining conversation by external commit', {conversationId, epoch});
if (epoch && epoch > 0) {
if (remoteEpoch && remoteEpoch > 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the truthy/falsy check here?

if (remoteEpoch && remoteEpoch > 0) conflates “missing”, “zero”, and “invalid” with normal control flow. Or in other words: undefined or null will skip joining and the method still resolves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants