Fix pairing system: Add waitForPairingCommitment with device sync and update documentation

- Add waitForPairingCommitment function with automatic device synchronization
- Integrate updateDevice() call in waitForPairingCommitment for better sync
- Increase retry attempts to 30 with 2s delay (60s total wait time)
- Add detailed logging for pairing process synchronization
- Update router to call waitForPairingCommitment before confirmPairing
- Remove redundant updateDevice() call from router
- Update PAIRING_SYSTEM_ANALYSIS.md with coherence issues and recommendations
- Identify joiner flow inconsistencies requiring future fixes
This commit is contained in:
NicolasCantu 2025-10-22 14:15:20 +02:00
parent 79633ed923
commit 7c2c4bfb46
7 changed files with 254143 additions and 99 deletions

View File

@ -0,0 +1,277 @@
# Analyse du Système de Pairing - Version Actuelle
## Vue d'ensemble
Ce document résume l'analyse complète du système de pairing et les corrections apportées pour résoudre les problèmes identifiés.
## Problèmes Identifiés et Solutions
### 1. Problème de `checkConnections` pour le Pairing
**Problème** : La méthode `checkConnections` a été mise à jour il y a un mois pour prendre un `Process` et un `stateId` au lieu d'une liste de membres, mais la gestion des processus de pairing était défaillante.
**Symptômes** :
- `checkConnections` échouait pour les processus de pairing
- Les adresses des membres n'étaient pas correctement récupérées
- Erreur "Not a pairing process" même pour des processus de pairing valides
**Solution Appliquée** :
```typescript
// Correction dans checkConnections pour gérer les pairedAddresses
if (members.size === 0) {
// This must be a pairing process
let publicData: Record<string, any> | null = null;
if (!stateId) {
publicData = process.states[process.states.length - 2]?.public_data;
} else {
publicData = process.states.find(state => state.state_id === stateId)?.public_data || null;
}
// If pairedAddresses is not in the current state, look in previous states
if (!publicData || !publicData['pairedAddresses']) {
// Look for pairedAddresses in previous states
for (let i = process.states.length - 1; i >= 0; i--) {
const state = process.states[i];
if (state.public_data && state.public_data['pairedAddresses']) {
publicData = state.public_data;
break;
}
}
}
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
members.add({ sp_addresses: decodedAddresses });
}
```
### 2. Problème de `confirmPairing` avec `getPairingProcessId()`
**Problème** : `confirmPairing` échouait car `getPairingProcessId()` utilisait `sdkClient.get_pairing_process_id()` qui n'était pas encore disponible car le processus de pairing n'était pas encore committé.
**Symptômes** :
- Erreur "Failed to get pairing process" dans `confirmPairing`
- Le SDK n'avait pas encore le processus de pairing disponible
- Échec de confirmation du pairing
**Solution Appliquée** :
```typescript
public async confirmPairing(pairingId?: string) {
try {
console.log('confirmPairing');
let processId: string;
if (pairingId) {
processId = pairingId;
console.log('pairingId (provided):', processId);
} else if (this.processId) {
processId = this.processId;
console.log('pairingId (from stored processId):', processId);
} else {
// Try to get pairing process ID, with retry if it fails
let retries = 3;
while (retries > 0) {
try {
processId = this.getPairingProcessId();
console.log('pairingId (from SDK):', processId);
break;
} catch (e) {
retries--;
if (retries === 0) throw e;
console.log(`Failed to get pairing process ID, retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
// ... rest of the method
} catch (e) {
console.error('Failed to confirm pairing');
return;
}
}
```
### 3. Problème de `pairing_process_commitment` à `null`
**Problème** : Le `pairing_process_commitment` restait à `null` dans le device dump car le device n'était pas synchronisé avec l'état committé du processus.
**Symptômes** :
- `pairing_process_commitment: null` dans le device dump
- Le commitment n'était pas synchronisé avec l'état committé du processus
- Échec de la confirmation du pairing
**Solution Appliquée** :
```typescript
// Intégration de updateDevice() dans waitForPairingCommitment
public async waitForPairingCommitment(processId: string, maxRetries: number = 10, retryDelay: number = 1000): Promise<void> {
console.log(`Waiting for pairing process ${processId} to be committed...`);
// First, try to update the device to sync with the committed state
try {
await this.updateDevice();
console.log('Device updated, checking commitment...');
} catch (e) {
console.log('Failed to update device, continuing with polling...', e);
}
for (let i = 0; i < maxRetries; i++) {
try {
const device = this.dumpDeviceFromMemory();
console.log(`Attempt ${i + 1}/${maxRetries}: pairing_process_commitment =`, device.pairing_process_commitment);
// Check if the commitment is set and not null/empty
if (device.pairing_process_commitment &&
device.pairing_process_commitment !== null &&
device.pairing_process_commitment !== '') {
console.log('Pairing process commitment found:', device.pairing_process_commitment);
return;
}
} catch (e) {
console.log(`Attempt ${i + 1}/${maxRetries}: Device not ready yet - ${e}`);
}
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
throw new Error(`Pairing process ${processId} was not committed after ${maxRetries} attempts`);
}
```
Et simplification du router :
```typescript
console.log("⏳ Waiting for pairing process to be committed...");
await services.waitForPairingCommitment(pairingId);
console.log("🔁 Confirming pairing...");
await services.confirmPairing(pairingId);
```
## Architecture du Système de Pairing
### Flux de Création du Pairing (Créateur)
1. **Création du processus** : `createPairingProcess("", [myAddress])`
2. **Enregistrement du device** : `pairDevice(pairingId, [myAddress])`
3. **Traitement de l'API** : `handleApiReturn(createPairingProcessReturn)`
4. **Création de la mise à jour PRD** : `createPrdUpdate(pairingId, stateId)`
5. **Approbation du changement** : `approveChange(pairingId, stateId)`
6. **Attente du commit avec synchronisation** : `waitForPairingCommitment(pairingId)` (inclut `updateDevice()`)
7. **Confirmation du pairing** : `confirmPairing(pairingId)`
### Flux de Rejoindre le Pairing (Joiner) - ⚠️ INCOHÉRENT
**Problème identifié** : Le joiner n'a pas de flux de confirmation complet.
**Flux actuel (incomplet)** :
1. **Création avec liste vide** : `createPairingProcess("", [])`
2. **Établissement des connexions** : `checkConnections(process)`
3. **Pas de confirmation** : Aucun `waitForPairingCommitment` ou `confirmPairing`
**Flux attendu (cohérent)** :
1. **Récupération du processus existant** : `getPairingProcessId()`
2. **Rejoindre le processus** : Pas de création, mais participation au processus existant
3. **Flux de confirmation complet** : Même flux que le créateur
4. **Attente du commit** : `waitForPairingCommitment()`
5. **Confirmation du pairing** : `confirmPairing()`
### Gestion des Connexions
La méthode `checkConnections` gère maintenant :
- **Processus normaux** : Utilise les rôles pour trouver les membres
- **Processus de pairing** : Utilise `pairedAddresses` des données publiques
- **Recherche dans les états précédents** : Si `pairedAddresses` n'est pas dans l'état actuel
- **Décodage des adresses** : Les données publiques sont encodées et nécessitent un décodage
## Points Clés Appris
### 1. Encodage des Données Publiques
- Les données publiques sont encodées avec `this.sdkClient.encode_json()`
- `pairedAddresses` nécessite un décodage avec `this.decodeValue()`
- Les données ne sont pas directement utilisables sans décodage
### 2. Gestion Multi-Hosts
- Le créateur et le joiner peuvent être sur des hosts différents
- Le joiner doit récupérer les adresses depuis le processus existant
- `this.processId` n'est disponible que sur le même host
### 3. Synchronisation du SDK
- Le SDK n'a pas immédiatement le processus de pairing disponible
- Il faut attendre que le processus soit committé
- `updateDevice()` est nécessaire pour synchroniser l'état
### 4. Gestion des États
- Les processus de pairing peuvent avoir des mises à jour partielles
- Il faut chercher `pairedAddresses` dans les états précédents si nécessaire
- La logique de fallback est cruciale pour la robustesse
## Version Actuelle
### État des Corrections
- ✅ `checkConnections` corrigé pour les processus de pairing
- ✅ `confirmPairing` avec gestion des paramètres et retry
- ✅ `waitForPairingCommitment` avec synchronisation automatique du device
- ✅ Intégration de `updateDevice()` dans `waitForPairingCommitment`
- ✅ Gestion des cas multi-hosts
- ✅ Simplification du flux de création
- ⚠️ **Problème identifié** : Incohérence entre créateur et joiner
### Fonctionnalités Opérationnelles
- **Création de pairing** : ✅ Fonctionne avec les adresses correctes
- **Rejoindre un pairing** : ❌ Flux incomplet, pas de confirmation
- **Établissement des connexions** : ✅ `checkConnections` trouve les membres
- **Confirmation du pairing** : ⚠️ Seulement côté créateur, pas côté joiner
- **Synchronisation du commitment** : ✅ Côté créateur, ❌ Côté joiner
- **Flux simplifié** : ✅ Côté créateur, ❌ Côté joiner
### Problèmes de Cohérence Identifiés
#### Incohérence Créateur vs Joiner
- **Créateur** : Flux complet avec 7 étapes incluant confirmation
- **Joiner** : Flux incomplet avec seulement 2 étapes, pas de confirmation
- **Impact** : Le joiner ne suit pas le même processus de validation
#### Problèmes Spécifiques du Joiner
1. **Création avec liste vide** : `createPairingProcess("", [])` ne permet pas de connexions
2. **Pas de flux de confirmation** : Aucun `waitForPairingCommitment` ou `confirmPairing`
3. **Pas de synchronisation** : Le `pairing_process_commitment` ne sera jamais défini
4. **Pas de validation** : Le pairing n'est pas validé côté joiner
### Améliorations Récentes
#### Synchronisation Automatique du Device
- **Intégration de `updateDevice()`** : Appelé automatiquement dans `waitForPairingCommitment`
- **Gestion des erreurs** : Continue le polling même si `updateDevice()` échoue
- **Logs détaillés** : Suivi complet du processus de synchronisation
- **Temps d'attente augmenté** : 30 tentatives × 2 secondes = 60 secondes max
#### Simplification du Flux
- **Moins d'étapes manuelles** : `updateDevice()` intégré dans `waitForPairingCommitment`
- **Flux plus robuste** : Gestion automatique de la synchronisation
- **Code plus maintenable** : Logique centralisée dans une seule méthode
### Points d'Attention
- Le système nécessite que les deux côtés soient synchronisés
- Les retry automatiques sont implémentés pour la robustesse
- La gestion des erreurs est améliorée avec des logs détaillés
- Le flux est maintenant plus prévisible et fiable
- La synchronisation du device est automatique et robuste
## Recommandations
### Corrections Prioritaires
1. **Corriger le flux du joiner** : Implémenter le même flux de confirmation que le créateur
2. **Unifier les processus** : Le joiner devrait rejoindre un processus existant, pas en créer un nouveau
3. **Synchronisation bidirectionnelle** : Les deux côtés doivent avoir le même niveau de validation
### Tests et Monitoring
1. **Tests** : Tester le pairing entre différents hosts avec les deux flux
2. **Monitoring** : Surveiller les logs pour identifier les problèmes potentiels
3. **Performance** : Optimiser les délais de retry si nécessaire
4. **Documentation** : Maintenir cette documentation à jour avec les évolutions
### Actions Immédiates
1. **Analyser le flux du joiner** : Comprendre comment il devrait rejoindre un processus existant
2. **Implémenter la cohérence** : Appliquer le même flux de confirmation aux deux côtés
3. **Valider la synchronisation** : S'assurer que les deux côtés ont le même `pairing_process_commitment`
Cette analyse fournit une base solide pour comprendre et maintenir le système de pairing, mais révèle des incohérences importantes qui doivent être corrigées.

View File

@ -5,7 +5,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client/pkg ../sdk_client --target bundler --dev", "build_wasm": "wasm-pack build --out-dir ../ihm_client_dev3/pkg ../sdk_client --target bundler --dev",
"start": "vite --host 0.0.0.0", "start": "vite --host 0.0.0.0",
"build": "tsc && vite build", "build": "tsc && vite build",
"deploy": "sudo cp -r dist/* /var/www/html/", "deploy": "sudo cp -r dist/* /var/www/html/",

253667
screenlog.0 Normal file

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,8 @@ export enum MessageType {
// Processes // Processes
CREATE_PROCESS = 'CREATE_PROCESS', CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED', PROCESS_CREATED = 'PROCESS_CREATED',
CREATE_CONVERSATION = 'CREATE_CONVERSATION',
CONVERSATION_CREATED = 'CONVERSATION_CREATED',
UPDATE_PROCESS = 'UPDATE_PROCESS', UPDATE_PROCESS = 'UPDATE_PROCESS',
PROCESS_UPDATED = 'PROCESS_UPDATED', PROCESS_UPDATED = 'PROCESS_UPDATED',
NOTIFY_UPDATE = 'NOTIFY_UPDATE', NOTIFY_UPDATE = 'NOTIFY_UPDATE',

View File

@ -286,8 +286,11 @@ export async function registerAllListeners() {
console.log("📜 Approve change result:", approveChangeReturn); console.log("📜 Approve change result:", approveChangeReturn);
await services.handleApiReturn(approveChangeReturn); await services.handleApiReturn(approveChangeReturn);
console.log("⏳ Waiting for pairing process to be committed...");
await services.waitForPairingCommitment(pairingId);
console.log("🔁 Confirming pairing..."); console.log("🔁 Confirming pairing...");
await services.confirmPairing(); await services.confirmPairing(pairingId);
console.log("🎉 Pairing successfully completed!"); console.log("🎉 Pairing successfully completed!");

View File

@ -123,6 +123,7 @@ export default class Services {
this.relayReadyResolver = null; this.relayReadyResolver = null;
this.relayReadyPromise = null; this.relayReadyPromise = null;
} }
}
public async addWebsocketConnection(url: string): Promise<void> { public async addWebsocketConnection(url: string): Promise<void> {
console.log('Opening new websocket connection'); console.log('Opening new websocket connection');
@ -255,7 +256,25 @@ export default class Services {
if (members.size === 0) { if (members.size === 0) {
// This must be a pairing process // This must be a pairing process
// Check if we have a pairedAddresses in the public data // Check if we have a pairedAddresses in the public data
const publicData = process.states[0]?.public_data; let publicData: Record<string, any> | null = null;
if (!stateId) {
publicData = process.states[process.states.length - 2]?.public_data;
} else {
publicData = process.states.find(state => state.state_id === stateId)?.public_data || null;
}
// If pairedAddresses is not in the current state, look in previous states
if (!publicData || !publicData['pairedAddresses']) {
// Look for pairedAddresses in previous states
for (let i = process.states.length - 1; i >= 0; i--) {
const state = process.states[i];
if (state.public_data && state.public_data['pairedAddresses']) {
publicData = state.public_data;
break;
}
}
}
if (!publicData || !publicData['pairedAddresses']) { if (!publicData || !publicData['pairedAddresses']) {
throw new Error('Not a pairing process'); throw new Error('Not a pairing process');
} }
@ -754,13 +773,79 @@ export default class Services {
} }
} }
public async confirmPairing() { public async waitForPairingCommitment(processId: string, maxRetries: number = 30, retryDelay: number = 2000): Promise<void> {
console.log(`⏳ Waiting for pairing process ${processId} to be committed and synchronized...`);
console.log(`🔄 This may take some time as we wait for SDK synchronization...`);
for (let i = 0; i < maxRetries; i++) {
try {
// Try to update device first (may fail if not committed yet)
try {
await this.updateDevice();
console.log(`✅ Device update successful on attempt ${i + 1}`);
} catch (e) {
console.log(`⚠️ Device update failed on attempt ${i + 1} (process may not be committed yet): ${e.message}`);
}
const device = this.dumpDeviceFromMemory();
console.log(`🔍 Attempt ${i + 1}/${maxRetries}: pairing_process_commitment =`, device.pairing_process_commitment);
// Check if the commitment is set and not null/empty
if (device.pairing_process_commitment &&
device.pairing_process_commitment !== null &&
device.pairing_process_commitment !== '') {
console.log('✅ Pairing process commitment found:', device.pairing_process_commitment);
return;
}
console.log(`⏳ Still waiting for SDK synchronization... (${i + 1}/${maxRetries})`);
} catch (e) {
console.log(`❌ Attempt ${i + 1}/${maxRetries}: Error during synchronization - ${e.message}`);
}
if (i < maxRetries - 1) {
console.log(`⏳ Waiting ${retryDelay}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
throw new Error(`❌ Pairing process ${processId} was not synchronized after ${maxRetries} attempts (${maxRetries * retryDelay / 1000}s)`);
}
public async confirmPairing(pairingId?: string) {
try { try {
// Is the wasm paired? // Is the wasm paired?
const pairingId = this.getPairingProcessId(); console.log('confirmPairing');
let processId: string;
if (pairingId) {
processId = pairingId;
console.log('pairingId (provided):', processId);
} else if (this.processId) {
processId = this.processId;
console.log('pairingId (from stored processId):', processId);
} else {
// Try to get pairing process ID, with retry if it fails
let retries = 3;
while (retries > 0) {
try {
processId = this.getPairingProcessId();
console.log('pairingId (from SDK):', processId);
break;
} catch (e) {
retries--;
if (retries === 0) {
throw e;
}
console.log(`Failed to get pairing process ID, retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retry
}
}
}
// TODO confirm that the pairing process id is known, commited // TODO confirm that the pairing process id is known, commited
const newDevice = this.dumpDeviceFromMemory(); const newDevice = this.dumpDeviceFromMemory();
console.log('newDevice:', newDevice);
await this.saveDeviceInDatabase(newDevice); await this.saveDeviceInDatabase(newDevice);
console.log('Device saved in database');
} catch (e) { } catch (e) {
console.error('Failed to confirm pairing'); console.error('Failed to confirm pairing');
return; return;
@ -1228,12 +1313,14 @@ export default class Services {
} }
async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<any | null> { async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<any | null> {
console.log(`[decryptAttribute] Starting decryption for attribute: ${attribute}, processId: ${processId}`);
let hash = state.pcd_commitment[attribute]; let hash = state.pcd_commitment[attribute];
if (!hash) { if (!hash) {
// attribute doesn't exist console.log(`[decryptAttribute] No hash found for attribute: ${attribute}`);
return null; return null;
} }
let key = state.keys[attribute]; let key = state.keys[attribute];
console.log(`[decryptAttribute] Initial key state for ${attribute}:`, key ? 'present' : 'missing');
const pairingProcessId = this.getPairingProcessId(); const pairingProcessId = this.getPairingProcessId();
// If key is missing, request an update and then retry // If key is missing, request an update and then retry
@ -1253,8 +1340,12 @@ export default class Services {
} }
} }
if (!hasAccess) return null; if (!hasAccess) {
console.log(`[decryptAttribute] No access rights for attribute: ${attribute}`);
return null;
}
console.log(`[decryptAttribute] Requesting key update for attribute: ${attribute}`);
await this.checkConnections((await this.getProcess(processId))!); await this.checkConnections((await this.getProcess(processId))!);
// We should have the key, so we're going to ask other members for it // We should have the key, so we're going to ask other members for it
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
@ -1264,17 +1355,21 @@ export default class Services {
let retries = 0; let retries = 0;
while ((!hash || !key) && retries < maxRetries) { while ((!hash || !key) && retries < maxRetries) {
console.log(`[decryptAttribute] Retry ${retries + 1}/${maxRetries} for attribute: ${attribute}`);
await new Promise(resolve => setTimeout(resolve, retryDelay)); await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting // Re-read hash and key after waiting
hash = state.pcd_commitment[attribute]; hash = state.pcd_commitment[attribute];
key = state.keys[attribute]; key = state.keys[attribute];
retries++; retries++;
console.log(`[decryptAttribute] After retry ${retries}: hash=${!!hash}, key=${!!key}`);
} }
} }
if (hash && key) { if (hash && key) {
console.log(`[decryptAttribute] Starting decryption process with hash: ${hash.substring(0, 8)}...`);
const blob = await this.getBlobFromDb(hash); const blob = await this.getBlobFromDb(hash);
if (blob) { if (blob) {
console.log(`[decryptAttribute] Blob retrieved successfully for ${attribute}`);
// Decrypt the data // Decrypt the data
const buf = await blob.arrayBuffer(); const buf = await blob.arrayBuffer();
const cipher = new Uint8Array(buf); const cipher = new Uint8Array(buf);
@ -1291,7 +1386,7 @@ export default class Services {
throw new Error('decrypt_data returned null'); throw new Error('decrypt_data returned null');
} }
} catch (e) { } catch (e) {
console.error(`Failed to decrypt data: ${e}`); console.error(`[decryptAttribute] Failed to decrypt data for ${attribute}:`, e);
} }
} }
} }

View File

@ -162,12 +162,6 @@ async function onCreateButtonClick() {
export async function prepareAndSendPairingTx(): Promise<void> { export async function prepareAndSendPairingTx(): Promise<void> {
const service = await Services.getInstance(); const service = await Services.getInstance();
try {
await service.checkConnections([]);
} catch (e) {
throw e;
}
try { try {
const relayAddress = service.getAllRelays(); const relayAddress = service.getAllRelays();
const createPairingProcessReturn = await service.createPairingProcess( const createPairingProcessReturn = await service.createPairingProcess(
@ -184,6 +178,12 @@ export async function prepareAndSendPairingTx(): Promise<void> {
await service.handleApiReturn(createPairingProcessReturn); await service.handleApiReturn(createPairingProcessReturn);
try {
await service.checkConnections(createPairingProcessReturn.updated_process.current_process);
} catch (e) {
throw e;
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }