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

E2e protocol ricardo 20231025 login sync #1780

Merged

Conversation

corrideat
Copy link
Member

No description provided.

@Silver-IT
Copy link
Member

Having checked this PR mainly focusing on the user flow, I could see several issues.

Issue 1

  • Steps to reproduce
    • [Tab1] user1 signs up, and creates a group
    • [Tab2] user2 joins the group, and closes the tab
    • [Tab1] user1 kicks the u2 from the group
    • [Tab2] Opens 'http://localhost:8000'. Got 5 errors in the console
    • [Tab1] Got error in console too.
  • Screenshot
    • Error in Tab1
      image
    • 5 Errors in Tab2
      image
      image
      image
      image
  • What should happen
    • No errors should be happening. And the important thing is that there MUST be no errors in Tab1.
    • gi.actions/group/join shouldn't be called. But it is called twice.
  • DeadLock Output
    • [Tab1] {}
    • [Tab2] {}

@Silver-IT
Copy link
Member

Issue 2

  • Steps to reproduce
    • [Tab1] user1 signs up, and creates a group
    • [Tab2] user2 joins the group, and closes the tab
    • [Tab1] user1 kicks the u2 from the group
    • [Tab3] user2 logs in, and got 5 errors.
  • Screenshot
    • 5 Errors in Tab2
      image
      image
      image
      image
      image
  • What should happen
    • No errors should be happening.
    • gi.actions/group/join should be called ONLY once. But it is called twice.
  • DeadLock Output
    • [Tab1] {}
    • [Tab2] {}

@Silver-IT
Copy link
Member

Issue 3

  • Steps to reproduce
    • [Tab1] user1 signs up, and creates a group.
    • [Tab2] user2 joins the group.
    • [Tab1] user1 kicks the u2 from the group. Got 1 error in console.
    • [Tab2] user2 joins the group again with the initial invite link.
    • [Tab2] Wrong work flow to join. 6 Errors in console. Seems there are many redundant function calls.
    • [Tab1] Many logs in console than expected. Seems there are too many function calls. And duplicated notifications.
  • Screenshot
    • Error in Tab1 (Step 3)
      image
    • Errors in Tab2 (Step 5)
      image
      image
    • Duplicated notifications in Tab1 (Step 6)
      image
  • What should happen
    • No errors should be happening.
    • No duplicated notifications in Tab1
    • gi.actions/group/join should be called ONLY once. But it is called 4 times.
  • DeadLock Output
    • [Tab1]
      image
    • [Tab2] {}

@Silver-IT
Copy link
Member

Issue 4

  • Steps to reproduce
    • [Tab1] user1 signs up, and creates a group.
    • [Tab2] user2 joins the group.
    • [Tab3] user3 joins the group, and closes the tab.
    • [Tab2] user2 creates a proposal to remove user3 from the group.
    • [Tab1] user1 approves the proposal, and kicks user3 from the group. Got an error in the notifications.
    • [Tab4] user3 tries to log in, and 8 errors BANG.
  • Screenshot
    • Error in Tab1 (Step 5)
      (user1 = alexjin, user2 = greg, user3 = andrea)
      image
    • Errors in Tab4 (Step 6)
      image
      image
      image
      image
      image
      image
      image
      image
  • What should happen
    • No errors should be happening.
    • gi.actions/group/join should be called ONLY once. But it is called twice in Tab4 (Step 6).
  • DeadLock Output
    • [Tab1]
      image
    • [Tab2] {}
    • [Tab4] {}

@Silver-IT
Copy link
Member

Issue 5

  • Steps to reproduce
    • [Tab1] user1 signs up, and creates a group.
    • [Tab2] user2 joins the group.
    • [Tab3] user1 logs in.
    • [Tab4] user2 logs in.
    • [Tab1] user1 kicks user2 from the group.
    • [Tab1], [Tab2], [Tab4] Errors BANG.
  • Screenshot
    • Error2 in Tab1 (Step 6)
      (user1 = alexjin, user2 = greg)
      image
    • Errors in Tab2 (Step 6)
      image
    • Errors in Tab4 (Step 6)
      image
  • What should happen
    • No errors should be happening.
  • DeadLock Output
    • [Tab1] {}
    • [Tab2] {}
    • [Tab3] {}
    • [Tab4] {}

@corrideat
Copy link
Member Author

Thanks again, @Silver-IT!

As discussed:

  1. I've made side-effects mostly work as they did earlier
  2. I've added a check on syncContractAndWatchKeys to avoid syncing contracts that have been subscribed to

In addition, I've implemented fixes for all of the issues reported here, except issue 3. I'll continue working on this until issue 3 is resolved.

Regarding issue 1, there is the double sync, that I've tracked down to the login functionality. I'll work on addressing this as well (but it seems to be the case that implementing the join|leaveGroup actions would help solving it too).

@taoeffect: You'll probably be pleased to see that I've mostly removed sync from recreateEvent, something you weren't entirely happy with. There are some vestiges of it that remain, because sometimes the pubsub functions don't work as expected and some events are never received. Once this is addressed in pubsub, the syncContract added to publishEvent can be removed.

@corrideat corrideat marked this pull request as ready for review November 3, 2023 18:10
@corrideat corrideat marked this pull request as draft November 3, 2023 18:11
@corrideat
Copy link
Member Author

@Silver-IT I've made another commit addressing issue 3. The solution so far is using sbp('okTurtles.data/set', 'JOINING_CHATROOM-' + params.contractID, ?) to avoid removing the chatroom contract when joining.

Besides that, two issues remain.

One is at leaving; specifically, when someone else leaves a chatroom we have left:

To write to a contract, we need to be subscribed to it. This is done in encryptedAction, but for contracts that call /remove in a side-effect, this is essentially pointless. This is an error.

The second issue is at joining. You'll see that the /join action calls /sync. This is 'unnecessary', as encryptedAction already does this. However, similarly to what happened above, the first /sync will be pointless because of the side-effect removing the contract, requiring a second call to sync that sets the JOINING_CHATROOM-${cID} key.

I think that we need to either adapt encryptedAction to take pre- and post-hooks (we could call them, e.g., prologue and epilogue) to handle this, or somehow re-think how /remove gets called (I think that one thing that would work is doing reference counting, but there may be other options too). These are not mutually exclusive, so we could very well do both.

Related to this, there is a third issue, which is a variant of issue 3:

  • rejoin fails
    • [tab1] u1: create group
    • [tab2] u2: join using invite link
    • [tab2] u2: close tab / navigate away
    • [tab1] u1: remove u2
    • [tab2] u2: open tab with invite link

To recap, apart from the issues mentioned, which are related to removing contracts, there is the double-sync issue I mentioned earlier.

Although double-syncs are a concern, I'll focus now on trying to address the actual issues that happen from missing contract states, which are errors.

Do you have a suggestion on how we could call /remove in an elegant way?

@corrideat corrideat force-pushed the e2e-protocol-ricardo-20231025-login-sync branch from c8cbd3b to 9364fdf Compare November 4, 2023 21:11
Copy link
Member

@Silver-IT Silver-IT left a comment

Choose a reason for hiding this comment

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

Nice work, @corrideat. I did a very partial review, and left a just two comments.
I can see many updates in contract parts (model/contracts, controller/actions) but I don't think most of them are needed. I have almost finished my PR #1728 and I want you to have even a brief look at it. I fixed most of the login issues with the smaller changes than this.

Also will check the 5 Issues (described above) again, and left comments here.

Comment on lines 996 to 998
sbp('chelonia/queueInvocation', contractID, () => sbp('gi.contracts/group/leaveGroup', { data, meta, contractID }, { state, getters })).catch(e => {
console.error(`[gi.contracts/group/removeMember/sideEffect] Error ${e.name} during queueInvocation for ${contractID}`, e)
})
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to call gi.contracts/group/leaveGroup when the member has joined the group after this action.
So it would be necessary to check the group contract profile state in this invocation.
Please reference my PR for this.

sideEffect ({ data, meta, contractID }, { state, getters }) {
  const rootState = sbp('state/vuex/state')
  const rootGetters = sbp('state/vuex/getters')
  const contracts = rootState.contracts || {}
  const { username } = rootState.loggedIn

  if (data.member === username) {
    // If this member is re-joining the group, ignore the rest
    // so the member doesn't remove themself again.
    sbp('chelonia/queueInvocation', contractID, async () => {
      if (rootState[contractID].profiles?.[username]?.status === PROFILE_STATUS.REMOVED) {
        // NOTE: should remove archived data from IndexedStorage
        //       regarding the current group (proposals, payments)
        await sbp('gi.contracts/group/removeArchivedProposals', contractID)
        await sbp('gi.contracts/group/removeArchivedPayments', contractID)

Comment on lines 316 to 320
preSendCheck: (_, state) => {
// Avoid sending a duplicate action if the person has already
// left or been removed from the chatroom
return !!state?.users?.[member]
}
Copy link
Member

Choose a reason for hiding this comment

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

Inside group contract, it doesn't make sense to decide the next operation depending on the chatroom contract state.

@Silver-IT
Copy link
Member

Silver-IT commented Nov 7, 2023

Issue 1 (#1780 (comment)) This issue is not fixed yet.
image
This screenshot shows the console logs filtered by gi.actions/group on Tab2 in Step 5 .

Problem: three errors and 2 redundant gi.actions/group/join function calls

@Silver-IT
Copy link
Member

Issue 2 (#1780 (comment)) This issue is not fixed yet.
image
This screenshot shows the console logs filtered by gi.actions/group/join on Tab3 in Step 4.

Problem: 1 redundant gi.actions/group/join function call.

@Silver-IT
Copy link
Member

Issue 3 (#1780 (comment)) seems not to be fixed yet.
image
image
image

Problem: two errors in console

Copy link
Member

@taoeffect taoeffect left a comment

Choose a reason for hiding this comment

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

Preliminary review

Comment on lines 375 to 377
} catch {
// Signature or encryption error
return undefined
Copy link
Member

Choose a reason for hiding this comment

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

No console.warn is needed here?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is doing the same as was it was doing earlier, more or less. There could be a console.warn, but that is already done elsewhere and it feels repetitive to warn multiple times.

Copy link
Member

Choose a reason for hiding this comment

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

Then a comment about that should be here...

@@ -407,7 +406,7 @@ export class GIMessage {
return `${desc}|${this.hash()} of ${this.contractID()}>`
}

isFirstMessage (): boolean { return !this.head().previousHEAD }
isFirstMessage (): boolean { return !this.head().contractID }
Copy link
Member

Choose a reason for hiding this comment

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

Why was this changed?

Copy link
Member Author

@corrideat corrideat Nov 7, 2023

Choose a reason for hiding this comment

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

Because I removed a bunch of the repetitive code in chelonia.js that was looking up the previous HEAD, which is now done by publishEvent. Since the previous HEAD could now momentarily be null, the check as it was would fail. Note that either condition is correct (if the previous HEAD were set).

@@ -36,7 +37,7 @@ export default ({
...mapState(['currentGroupId']),
ourGroupProfile () {
if (!this.ephemeral.groupIdWhenMounted) return
return this.$store.state[this.ephemeral.groupIdWhenMounted]?.profiles?.[this.ourUsername]
return this.$store.state[this.ephemeral.groupIdWhenMounted]?.profiles?.[this.ourUsername]?.status === PROFILE_STATUS.ACTIVE
Copy link
Member

Choose a reason for hiding this comment

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

This change changes the meaning of this computed property.

The computed property is named ourGroupProfile, which implies it returns our group profile. But now this no longer happens, instead it returns a boolean.

So either the condition needs to be removed or the name of the computed property needs to be changed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed.

Comment on lines 26 to 36

export class GIErrorMissingSigningKeyError extends Error {
constructor (...params: any[]) {
super(...params)
// this.name = this.constructor.name
this.name = 'GIErrorMissingSigningKeyError' // string literal so minifier doesn't overwrite
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a good time to copy over that utility function from Chelonia to define these using 1-liners...

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed.

Comment on lines 321 to 326

// TODO: The [pubsub] code seems to miss events that happened between
// a call to sync and the subscription time. This is a temporary measure
// to handle this until [pubsub] is updated.
if (entry.height() === lastAttemptedHeight) {
await sbp('okTurtles.eventQueue/queueEvent', entry.contractID(), ['chelonia/private/in/syncContract', entry.contractID()])
Copy link
Member

Choose a reason for hiding this comment

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

So I agree that we should address this in this PR... one way to go about it is to make sure the pubsub is connected before 'chelonia/contract/sync' does the sync.

We might also want to verify that we are in fact connected to the pubsub server before allowing messages to be sent.

memberKey, member, profile, active: (profile: any)?.status === PROFILE_STATUS.ACTIVE
})
if (memberKey === member && (profile: any)?.status === PROFILE_STATUS.ACTIVE) {
return [memberKey, { status: PROFILE_STATUS.REMOVED }]
Copy link
Member

Choose a reason for hiding this comment

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

This line seems to be removing many profile fields... that seems wrong.

Copy link
Member Author

Choose a reason for hiding this comment

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

There isn't much in the profile and this is for removed members. Would you prefer { ...profile, status: PROFILE_STATUS.REMOVED } instead?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think that would be much more robust.

Comment on lines +293 to +302
// When a group is being left, we want to also leave chatrooms,
// including private chatrooms. Since the user issuing the action
// may not be a member of the chatroom, we use the group's CSK
// unconditionally in this situation, which should be a key in the
// chatroom (either the CSK or the groupKey)
if (leavingGroup) {
const signingKeyId = sbp('chelonia/contract/currentKeyIdByName', state, 'csk', true)

// If we don't have a CSK, it is because we've already been removed.
// Proceeding would cause an error
Copy link
Member

Choose a reason for hiding this comment

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

These are good comments 👍

}

extraParams.signingKeyId = signingKeyId
extraParams.innerSigningContractID = null
Copy link
Member

Choose a reason for hiding this comment

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

Does innerSigningContractID = null need to be specified? If yes, could you add a comment explaining why?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it needs to be specified or else it'll be set to the identity CSK.

Comment on lines 488 to 490
const newOp = opT === GIMessage.OP_ATOMIC && (newRawOpV: any).length === 1
? (newRawOpV: any)[0]
: [opT, newRawOpV]
Copy link
Member

@taoeffect taoeffect Nov 7, 2023

Choose a reason for hiding this comment

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

What's going on here? Are you sure this is right? Is ? (newRawOpV: any)[0] correct?

Why is this being done? Is it rewriting what the user is sending? We should probably keep the developer intent and not modify messages the developer is sending.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reverted

}
return Promise.resolve(sbp(`${manifestHash}/${action}/sideEffect`, mutation))
Copy link
Member

Choose a reason for hiding this comment

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

Why is this wrapping the result in Promise.resolve? What if an error occurred? What's the point of later checking .then and Promise.allSettled if they're guaranteed to settled because of the Promise.resolve?

Or is that not how it works? If so please add a comment explaining because this is confusing...

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed

@Silver-IT
Copy link
Member

  * Pass params from sync to syncContract
  * Use fresh state in signData and encryptData
  * Check membership status in Join.vue
@Silver-IT
Copy link
Member

@corrideat, @taoeffect. I have just checked the test scenario.

Still existing issues in terms of workflow

  • Issue 1
    Problem:

    • user2 has successfully logged in.
    • One error occurred.
      image
  • Issue 2
    Problem:

    • Two errors occurred.
    • gi.actions/group/join is called twice. It should be called only once.
      image
      image
      image
  • Issue 3
    Problem:

    • Error in [Tab2] after being kicked from the group. (Not before to try join)
    • Three gi.actions/group/join calls. Only two calls are needed.
      image
      image
  • Issue 4
    Problem:

    • 4 errors in console in Tab4
    • gi.actions/group/join is called twice. It should be called only once.
      image
      image
  • Issue 5
    Seems fine.

(user1 = alexjin, user2 = greg, user3 = andrea)

IMHO, I don't approve the many updates in model/contracts and controllers/actions. Especially, I can't approve the way to leave chatroom when leaving group.

Copy link
Member

@taoeffect taoeffect left a comment

Choose a reason for hiding this comment

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

This is looking really solid now, thanks @Silver-IT for finding those issues and @corrideat for figuring out the fixes! I think it's ready to merge into e2e-protocol but with also double-check with @Silver-IT to see if he can confirm that the issues he found have been fixed.

@@ -926,8 +969,8 @@ export default (sbp('sbp/selectors/register', {
// of waiting, we need to remove it ourselves
const keyId = findKeyIdByName(contractState, keyName)
if (!keyId) {
console.log('@@@@ pendingWatch --- pushed to keysToDelete', { contractID, externalContractID, keyName, state: cloneDeep(contractState) })
Copy link
Member

Choose a reason for hiding this comment

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

Is this logging still needed?

@corrideat corrideat marked this pull request as ready for review November 17, 2023 17:16
@taoeffect taoeffect merged commit baacd80 into e2e-protocol-ricardo Nov 17, 2023
2 checks passed
@taoeffect taoeffect deleted the e2e-protocol-ricardo-20231025-login-sync branch November 17, 2023 17:16
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.

3 participants