@brightchain/brightchain-lib 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/lib/db/aggregation.d.ts +20 -0
- package/src/lib/db/aggregation.d.ts.map +1 -0
- package/src/lib/db/aggregation.js +407 -0
- package/src/lib/db/aggregation.js.map +1 -0
- package/src/lib/db/collection.d.ts +315 -0
- package/src/lib/db/collection.d.ts.map +1 -0
- package/src/lib/db/collection.js +1054 -0
- package/src/lib/db/collection.js.map +1 -0
- package/src/lib/db/cursor.d.ts +51 -0
- package/src/lib/db/cursor.d.ts.map +1 -0
- package/src/lib/db/cursor.js +100 -0
- package/src/lib/db/cursor.js.map +1 -0
- package/src/lib/db/errors.d.ts +83 -0
- package/src/lib/db/errors.d.ts.map +1 -0
- package/src/lib/db/errors.js +103 -0
- package/src/lib/db/errors.js.map +1 -0
- package/src/lib/db/inMemoryDatabase.d.ts +135 -0
- package/src/lib/db/inMemoryDatabase.d.ts.map +1 -0
- package/src/lib/db/inMemoryDatabase.js +258 -0
- package/src/lib/db/inMemoryDatabase.js.map +1 -0
- package/src/lib/db/inMemoryHeadRegistry.d.ts +33 -0
- package/src/lib/db/inMemoryHeadRegistry.d.ts.map +1 -0
- package/src/lib/db/inMemoryHeadRegistry.js +83 -0
- package/src/lib/db/inMemoryHeadRegistry.js.map +1 -0
- package/src/lib/db/index.d.ts +14 -0
- package/src/lib/db/index.d.ts.map +1 -0
- package/src/lib/db/index.js +18 -0
- package/src/lib/db/index.js.map +1 -0
- package/src/lib/db/indexing.d.ts +133 -0
- package/src/lib/db/indexing.d.ts.map +1 -0
- package/src/lib/db/indexing.js +287 -0
- package/src/lib/db/indexing.js.map +1 -0
- package/src/lib/db/queryEngine.d.ts +50 -0
- package/src/lib/db/queryEngine.d.ts.map +1 -0
- package/src/lib/db/queryEngine.js +461 -0
- package/src/lib/db/queryEngine.js.map +1 -0
- package/src/lib/db/schemaValidation.d.ts +41 -0
- package/src/lib/db/schemaValidation.d.ts.map +1 -0
- package/src/lib/db/schemaValidation.js +322 -0
- package/src/lib/db/schemaValidation.js.map +1 -0
- package/src/lib/db/transaction.d.ts +88 -0
- package/src/lib/db/transaction.d.ts.map +1 -0
- package/src/lib/db/transaction.js +112 -0
- package/src/lib/db/transaction.js.map +1 -0
- package/src/lib/db/types.d.ts +11 -0
- package/src/lib/db/types.d.ts.map +1 -0
- package/src/lib/db/types.js +11 -0
- package/src/lib/db/types.js.map +1 -0
- package/src/lib/db/updateEngine.d.ts +20 -0
- package/src/lib/db/updateEngine.d.ts.map +1 -0
- package/src/lib/db/updateEngine.js +193 -0
- package/src/lib/db/updateEngine.js.map +1 -0
- package/src/lib/db/uuidGenerator.d.ts +13 -0
- package/src/lib/db/uuidGenerator.d.ts.map +1 -0
- package/src/lib/db/uuidGenerator.js +34 -0
- package/src/lib/db/uuidGenerator.js.map +1 -0
- package/src/lib/documents/member/memberProfileHydration.d.ts.map +1 -1
- package/src/lib/documents/member/memberProfileHydration.js +6 -0
- package/src/lib/documents/member/memberProfileHydration.js.map +1 -1
- package/src/lib/enumerations/brightChainStrings.d.ts +39 -0
- package/src/lib/enumerations/brightChainStrings.d.ts.map +1 -1
- package/src/lib/enumerations/brightChainStrings.js +47 -0
- package/src/lib/enumerations/brightChainStrings.js.map +1 -1
- package/src/lib/enumerations/identityValidationErrorType.d.ts +11 -0
- package/src/lib/enumerations/identityValidationErrorType.d.ts.map +1 -0
- package/src/lib/enumerations/identityValidationErrorType.js +15 -0
- package/src/lib/enumerations/identityValidationErrorType.js.map +1 -0
- package/src/lib/enumerations/index.d.ts +4 -0
- package/src/lib/enumerations/index.d.ts.map +1 -1
- package/src/lib/enumerations/index.js +5 -0
- package/src/lib/enumerations/index.js.map +1 -1
- package/src/lib/enumerations/memberStatusType.d.ts +2 -1
- package/src/lib/enumerations/memberStatusType.d.ts.map +1 -1
- package/src/lib/enumerations/memberStatusType.js +1 -0
- package/src/lib/enumerations/memberStatusType.js.map +1 -1
- package/src/lib/enumerations/proposalActionType.d.ts +22 -0
- package/src/lib/enumerations/proposalActionType.d.ts.map +1 -0
- package/src/lib/enumerations/proposalActionType.js +26 -0
- package/src/lib/enumerations/proposalActionType.js.map +1 -0
- package/src/lib/enumerations/proposalStatus.d.ts +14 -0
- package/src/lib/enumerations/proposalStatus.d.ts.map +1 -0
- package/src/lib/enumerations/proposalStatus.js +18 -0
- package/src/lib/enumerations/proposalStatus.js.map +1 -0
- package/src/lib/enumerations/quorumErrorType.d.ts +30 -1
- package/src/lib/enumerations/quorumErrorType.d.ts.map +1 -1
- package/src/lib/enumerations/quorumErrorType.js +37 -0
- package/src/lib/enumerations/quorumErrorType.js.map +1 -1
- package/src/lib/enumerations/quorumOperationalMode.d.ts +16 -0
- package/src/lib/enumerations/quorumOperationalMode.d.ts.map +1 -0
- package/src/lib/enumerations/quorumOperationalMode.js +20 -0
- package/src/lib/enumerations/quorumOperationalMode.js.map +1 -0
- package/src/lib/enumerations/sealingErrorType.d.ts +3 -1
- package/src/lib/enumerations/sealingErrorType.d.ts.map +1 -1
- package/src/lib/enumerations/sealingErrorType.js +2 -0
- package/src/lib/enumerations/sealingErrorType.js.map +1 -1
- package/src/lib/errors/identityValidationError.d.ts +8 -0
- package/src/lib/errors/identityValidationError.d.ts.map +1 -0
- package/src/lib/errors/identityValidationError.js +26 -0
- package/src/lib/errors/identityValidationError.js.map +1 -0
- package/src/lib/errors/index.d.ts +4 -0
- package/src/lib/errors/index.d.ts.map +1 -1
- package/src/lib/errors/index.js +7 -0
- package/src/lib/errors/index.js.map +1 -1
- package/src/lib/errors/memberIndexSchemaValidationError.d.ts +6 -0
- package/src/lib/errors/memberIndexSchemaValidationError.d.ts.map +1 -0
- package/src/lib/errors/memberIndexSchemaValidationError.js +15 -0
- package/src/lib/errors/memberIndexSchemaValidationError.js.map +1 -0
- package/src/lib/errors/quorumError.d.ts.map +1 -1
- package/src/lib/errors/quorumError.js +37 -0
- package/src/lib/errors/quorumError.js.map +1 -1
- package/src/lib/errors/sealingError.d.ts.map +1 -1
- package/src/lib/errors/sealingError.js +2 -0
- package/src/lib/errors/sealingError.js.map +1 -1
- package/src/lib/i18n/strings/englishUs.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUs.js +45 -0
- package/src/lib/i18n/strings/englishUs.js.map +1 -1
- package/src/lib/i18n/strings/french.d.ts.map +1 -1
- package/src/lib/i18n/strings/french.js +37 -0
- package/src/lib/i18n/strings/french.js.map +1 -1
- package/src/lib/i18n/strings/german.d.ts.map +1 -1
- package/src/lib/i18n/strings/german.js +37 -0
- package/src/lib/i18n/strings/german.js.map +1 -1
- package/src/lib/i18n/strings/japanese.d.ts.map +1 -1
- package/src/lib/i18n/strings/japanese.js +37 -0
- package/src/lib/i18n/strings/japanese.js.map +1 -1
- package/src/lib/i18n/strings/mandarin.d.ts.map +1 -1
- package/src/lib/i18n/strings/mandarin.js +37 -0
- package/src/lib/i18n/strings/mandarin.js.map +1 -1
- package/src/lib/i18n/strings/spanish.d.ts.map +1 -1
- package/src/lib/i18n/strings/spanish.js +37 -0
- package/src/lib/i18n/strings/spanish.js.map +1 -1
- package/src/lib/i18n/strings/ukrainian.d.ts.map +1 -1
- package/src/lib/i18n/strings/ukrainian.js +37 -0
- package/src/lib/i18n/strings/ukrainian.js.map +1 -1
- package/src/lib/index.d.ts +12 -0
- package/src/lib/index.d.ts.map +1 -1
- package/src/lib/index.js +55 -1
- package/src/lib/index.js.map +1 -1
- package/src/lib/interfaces/aliasRecord.d.ts +34 -0
- package/src/lib/interfaces/aliasRecord.d.ts.map +1 -0
- package/src/lib/interfaces/aliasRecord.js +11 -0
- package/src/lib/interfaces/aliasRecord.js.map +1 -0
- package/src/lib/interfaces/api/index.d.ts +2 -0
- package/src/lib/interfaces/api/index.d.ts.map +1 -0
- package/src/lib/interfaces/api/index.js +5 -0
- package/src/lib/interfaces/api/index.js.map +1 -0
- package/src/lib/interfaces/api/quorumApi.d.ts +97 -0
- package/src/lib/interfaces/api/quorumApi.d.ts.map +1 -0
- package/src/lib/interfaces/api/quorumApi.js +12 -0
- package/src/lib/interfaces/api/quorumApi.js.map +1 -0
- package/src/lib/interfaces/auditLogEntry.d.ts +34 -0
- package/src/lib/interfaces/auditLogEntry.d.ts.map +1 -0
- package/src/lib/interfaces/auditLogEntry.js +10 -0
- package/src/lib/interfaces/auditLogEntry.js.map +1 -0
- package/src/lib/interfaces/availability/gossipService.d.ts +116 -2
- package/src/lib/interfaces/availability/gossipService.d.ts.map +1 -1
- package/src/lib/interfaces/availability/gossipService.js +62 -0
- package/src/lib/interfaces/availability/gossipService.js.map +1 -1
- package/src/lib/interfaces/chainedAuditLogEntry.d.ts +27 -0
- package/src/lib/interfaces/chainedAuditLogEntry.d.ts.map +1 -0
- package/src/lib/interfaces/chainedAuditLogEntry.js +12 -0
- package/src/lib/interfaces/chainedAuditLogEntry.js.map +1 -0
- package/src/lib/interfaces/contentWithIdentity.d.ts +39 -0
- package/src/lib/interfaces/contentWithIdentity.d.ts.map +1 -0
- package/src/lib/interfaces/contentWithIdentity.js +24 -0
- package/src/lib/interfaces/contentWithIdentity.js.map +1 -0
- package/src/lib/interfaces/energyAccount.d.ts +3 -1
- package/src/lib/interfaces/energyAccount.d.ts.map +1 -1
- package/src/lib/interfaces/identityRecoveryRecord.d.ts +41 -0
- package/src/lib/interfaces/identityRecoveryRecord.d.ts.map +1 -0
- package/src/lib/interfaces/identityRecoveryRecord.js +11 -0
- package/src/lib/interfaces/identityRecoveryRecord.js.map +1 -0
- package/src/lib/interfaces/index.d.ts +17 -0
- package/src/lib/interfaces/index.d.ts.map +1 -1
- package/src/lib/interfaces/index.js +4 -0
- package/src/lib/interfaces/index.js.map +1 -1
- package/src/lib/interfaces/initResult.d.ts +6 -6
- package/src/lib/interfaces/initResult.d.ts.map +1 -1
- package/src/lib/interfaces/member/brightChainBaseInitResult.d.ts +4 -1
- package/src/lib/interfaces/member/brightChainBaseInitResult.d.ts.map +1 -1
- package/src/lib/interfaces/member/brightChainInitResult.d.ts +1 -1
- package/src/lib/interfaces/member/brightChainInitResult.d.ts.map +1 -1
- package/src/lib/interfaces/member/memberData.d.ts +3 -0
- package/src/lib/interfaces/member/memberData.d.ts.map +1 -1
- package/src/lib/interfaces/member/profileStorage.d.ts +5 -0
- package/src/lib/interfaces/member/profileStorage.d.ts.map +1 -1
- package/src/lib/interfaces/member-init-config.d.ts +20 -0
- package/src/lib/interfaces/member-init-config.d.ts.map +1 -0
- package/src/lib/interfaces/member-init-config.js +3 -0
- package/src/lib/interfaces/member-init-config.js.map +1 -0
- package/src/lib/interfaces/operationalState.d.ts +20 -0
- package/src/lib/interfaces/operationalState.d.ts.map +1 -0
- package/src/lib/interfaces/operationalState.js +10 -0
- package/src/lib/interfaces/operationalState.js.map +1 -0
- package/src/lib/interfaces/proposal.d.ts +59 -0
- package/src/lib/interfaces/proposal.d.ts.map +1 -0
- package/src/lib/interfaces/proposal.js +10 -0
- package/src/lib/interfaces/proposal.js.map +1 -0
- package/src/lib/interfaces/quorumDocumentMetadata.d.ts +20 -0
- package/src/lib/interfaces/quorumDocumentMetadata.d.ts.map +1 -0
- package/src/lib/interfaces/quorumDocumentMetadata.js +10 -0
- package/src/lib/interfaces/quorumDocumentMetadata.js.map +1 -0
- package/src/lib/interfaces/quorumEpoch.d.ts +33 -0
- package/src/lib/interfaces/quorumEpoch.d.ts.map +1 -0
- package/src/lib/interfaces/quorumEpoch.js +11 -0
- package/src/lib/interfaces/quorumEpoch.js.map +1 -0
- package/src/lib/interfaces/quorumMetrics.d.ts +49 -0
- package/src/lib/interfaces/quorumMetrics.d.ts.map +1 -0
- package/src/lib/interfaces/quorumMetrics.js +10 -0
- package/src/lib/interfaces/quorumMetrics.js.map +1 -0
- package/src/lib/interfaces/redistributionJournalEntry.d.ts +25 -0
- package/src/lib/interfaces/redistributionJournalEntry.d.ts.map +1 -0
- package/src/lib/interfaces/redistributionJournalEntry.js +11 -0
- package/src/lib/interfaces/redistributionJournalEntry.js.map +1 -0
- package/src/lib/interfaces/responses/backupCodesResponseData.d.ts +3 -5
- package/src/lib/interfaces/responses/backupCodesResponseData.d.ts.map +1 -1
- package/src/lib/interfaces/responses/challengeResponseData.d.ts +5 -0
- package/src/lib/interfaces/responses/challengeResponseData.d.ts.map +1 -1
- package/src/lib/interfaces/responses/codeCountResponseData.d.ts +3 -5
- package/src/lib/interfaces/responses/codeCountResponseData.d.ts.map +1 -1
- package/src/lib/interfaces/responses/index.d.ts +4 -2
- package/src/lib/interfaces/responses/index.d.ts.map +1 -1
- package/src/lib/interfaces/responses/passwordChangeResponse.d.ts +2 -0
- package/src/lib/interfaces/responses/passwordChangeResponse.d.ts.map +1 -0
- package/src/lib/interfaces/responses/passwordChangeResponse.js +3 -0
- package/src/lib/interfaces/responses/passwordChangeResponse.js.map +1 -0
- package/src/lib/interfaces/responses/recoveryResponse.d.ts +2 -0
- package/src/lib/interfaces/responses/recoveryResponse.d.ts.map +1 -0
- package/src/lib/interfaces/responses/recoveryResponse.js +3 -0
- package/src/lib/interfaces/responses/recoveryResponse.js.map +1 -0
- package/src/lib/interfaces/responses/registrationResponseData.d.ts +2 -2
- package/src/lib/interfaces/responses/registrationResponseData.d.ts.map +1 -1
- package/src/lib/interfaces/services/contentIngestion.d.ts +61 -0
- package/src/lib/interfaces/services/contentIngestion.d.ts.map +1 -0
- package/src/lib/interfaces/services/contentIngestion.js +12 -0
- package/src/lib/interfaces/services/contentIngestion.js.map +1 -0
- package/src/lib/interfaces/services/expirationScheduler.d.ts +55 -0
- package/src/lib/interfaces/services/expirationScheduler.d.ts.map +1 -0
- package/src/lib/interfaces/services/expirationScheduler.js +11 -0
- package/src/lib/interfaces/services/expirationScheduler.js.map +1 -0
- package/src/lib/interfaces/services/identitySealingPipeline.d.ts +56 -0
- package/src/lib/interfaces/services/identitySealingPipeline.d.ts.map +1 -0
- package/src/lib/interfaces/services/identitySealingPipeline.js +12 -0
- package/src/lib/interfaces/services/identitySealingPipeline.js.map +1 -0
- package/src/lib/interfaces/services/identityValidator.d.ts +44 -0
- package/src/lib/interfaces/services/identityValidator.d.ts.map +1 -0
- package/src/lib/interfaces/services/identityValidator.js +11 -0
- package/src/lib/interfaces/services/identityValidator.js.map +1 -0
- package/src/lib/interfaces/services/index.d.ts +9 -0
- package/src/lib/interfaces/services/index.d.ts.map +1 -1
- package/src/lib/interfaces/services/membershipProof.d.ts +40 -0
- package/src/lib/interfaces/services/membershipProof.d.ts.map +1 -0
- package/src/lib/interfaces/services/membershipProof.js +11 -0
- package/src/lib/interfaces/services/membershipProof.js.map +1 -0
- package/src/lib/interfaces/services/operatorPrompt.d.ts +68 -0
- package/src/lib/interfaces/services/operatorPrompt.d.ts.map +1 -0
- package/src/lib/interfaces/services/operatorPrompt.js +11 -0
- package/src/lib/interfaces/services/operatorPrompt.js.map +1 -0
- package/src/lib/interfaces/services/quorumDatabase.d.ts +207 -0
- package/src/lib/interfaces/services/quorumDatabase.d.ts.map +1 -0
- package/src/lib/interfaces/services/quorumDatabase.js +13 -0
- package/src/lib/interfaces/services/quorumDatabase.js.map +1 -0
- package/src/lib/interfaces/services/quorumService.d.ts +3 -0
- package/src/lib/interfaces/services/quorumService.d.ts.map +1 -1
- package/src/lib/interfaces/services/quorumStateMachine.d.ts +128 -0
- package/src/lib/interfaces/services/quorumStateMachine.d.ts.map +1 -0
- package/src/lib/interfaces/services/quorumStateMachine.js +12 -0
- package/src/lib/interfaces/services/quorumStateMachine.js.map +1 -0
- package/src/lib/interfaces/services/redistributionConfig.d.ts +20 -0
- package/src/lib/interfaces/services/redistributionConfig.d.ts.map +1 -0
- package/src/lib/interfaces/services/redistributionConfig.js +10 -0
- package/src/lib/interfaces/services/redistributionConfig.js.map +1 -0
- package/src/lib/interfaces/statuteConfig.d.ts +22 -0
- package/src/lib/interfaces/statuteConfig.d.ts.map +1 -0
- package/src/lib/interfaces/statuteConfig.js +18 -0
- package/src/lib/interfaces/statuteConfig.js.map +1 -0
- package/src/lib/interfaces/storage/documentStore.d.ts +46 -24
- package/src/lib/interfaces/storage/documentStore.d.ts.map +1 -1
- package/src/lib/interfaces/storage/documentStore.js +6 -2
- package/src/lib/interfaces/storage/documentStore.js.map +1 -1
- package/src/lib/interfaces/storage/index.d.ts +5 -0
- package/src/lib/interfaces/storage/index.d.ts.map +1 -1
- package/src/lib/interfaces/storage/index.js.map +1 -1
- package/src/lib/interfaces/storage/memberIndexSchema.d.ts +11 -0
- package/src/lib/interfaces/storage/memberIndexSchema.d.ts.map +1 -0
- package/src/lib/interfaces/storage/memberIndexSchema.js +26 -0
- package/src/lib/interfaces/storage/memberIndexSchema.js.map +1 -0
- package/src/lib/interfaces/storage/mnemonicSchema.d.ts +10 -0
- package/src/lib/interfaces/storage/mnemonicSchema.d.ts.map +1 -0
- package/src/lib/interfaces/storage/mnemonicSchema.js +22 -0
- package/src/lib/interfaces/storage/mnemonicSchema.js.map +1 -0
- package/src/lib/interfaces/storage/roleSchema.d.ts +10 -0
- package/src/lib/interfaces/storage/roleSchema.d.ts.map +1 -0
- package/src/lib/interfaces/storage/roleSchema.js +45 -0
- package/src/lib/interfaces/storage/roleSchema.js.map +1 -0
- package/src/lib/interfaces/storage/userRoleSchema.d.ts +10 -0
- package/src/lib/interfaces/storage/userRoleSchema.d.ts.map +1 -0
- package/src/lib/interfaces/storage/userRoleSchema.js +35 -0
- package/src/lib/interfaces/storage/userRoleSchema.js.map +1 -0
- package/src/lib/interfaces/storage/userSchema.d.ts +12 -0
- package/src/lib/interfaces/storage/userSchema.d.ts.map +1 -0
- package/src/lib/interfaces/storage/userSchema.js +62 -0
- package/src/lib/interfaces/storage/userSchema.js.map +1 -0
- package/src/lib/interfaces/userManagement.d.ts +49 -0
- package/src/lib/interfaces/userManagement.d.ts.map +1 -0
- package/src/lib/interfaces/userManagement.js +9 -0
- package/src/lib/interfaces/userManagement.js.map +1 -0
- package/src/lib/interfaces/vote.d.ts +45 -0
- package/src/lib/interfaces/vote.d.ts.map +1 -0
- package/src/lib/interfaces/vote.js +10 -0
- package/src/lib/interfaces/vote.js.map +1 -0
- package/src/lib/quorumDataRecord.d.ts +7 -1
- package/src/lib/quorumDataRecord.d.ts.map +1 -1
- package/src/lib/quorumDataRecord.js +12 -4
- package/src/lib/quorumDataRecord.js.map +1 -1
- package/src/lib/quorumDataRecordDto.d.ts +6 -0
- package/src/lib/quorumDataRecordDto.d.ts.map +1 -1
- package/src/lib/services/aliasRegistry.d.ts +77 -0
- package/src/lib/services/aliasRegistry.d.ts.map +1 -0
- package/src/lib/services/aliasRegistry.js +138 -0
- package/src/lib/services/aliasRegistry.js.map +1 -0
- package/src/lib/services/auditLogService.d.ts +100 -0
- package/src/lib/services/auditLogService.d.ts.map +1 -0
- package/src/lib/services/auditLogService.js +223 -0
- package/src/lib/services/auditLogService.js.map +1 -0
- package/src/lib/services/blockService.d.ts +2 -1
- package/src/lib/services/blockService.d.ts.map +1 -1
- package/src/lib/services/blockService.js +7 -2
- package/src/lib/services/blockService.js.map +1 -1
- package/src/lib/services/identitySealingPipeline.d.ts +120 -0
- package/src/lib/services/identitySealingPipeline.d.ts.map +1 -0
- package/src/lib/services/identitySealingPipeline.js +288 -0
- package/src/lib/services/identitySealingPipeline.js.map +1 -0
- package/src/lib/services/identityValidator.d.ts +75 -0
- package/src/lib/services/identityValidator.d.ts.map +1 -0
- package/src/lib/services/identityValidator.js +202 -0
- package/src/lib/services/identityValidator.js.map +1 -0
- package/src/lib/services/index.d.ts +6 -0
- package/src/lib/services/index.d.ts.map +1 -1
- package/src/lib/services/index.js +6 -0
- package/src/lib/services/index.js.map +1 -1
- package/src/lib/services/member/memberCblService.d.ts.map +1 -1
- package/src/lib/services/member/memberCblService.js +12 -1
- package/src/lib/services/member/memberCblService.js.map +1 -1
- package/src/lib/services/memberStore.d.ts.map +1 -1
- package/src/lib/services/memberStore.js +2 -0
- package/src/lib/services/memberStore.js.map +1 -1
- package/src/lib/services/membershipProofService.d.ts +90 -0
- package/src/lib/services/membershipProofService.d.ts.map +1 -0
- package/src/lib/services/membershipProofService.js +361 -0
- package/src/lib/services/membershipProofService.js.map +1 -0
- package/src/lib/services/quorumStateMachine.d.ts +336 -0
- package/src/lib/services/quorumStateMachine.d.ts.map +1 -0
- package/src/lib/services/quorumStateMachine.js +1396 -0
- package/src/lib/services/quorumStateMachine.js.map +1 -0
- package/src/lib/services/sealing.service.d.ts +80 -0
- package/src/lib/services/sealing.service.d.ts.map +1 -1
- package/src/lib/services/sealing.service.js +192 -0
- package/src/lib/services/sealing.service.js.map +1 -1
- package/src/lib/stores/energyAccountStore.d.ts +13 -11
- package/src/lib/stores/energyAccountStore.d.ts.map +1 -1
- package/src/lib/stores/energyAccountStore.js +18 -20
- package/src/lib/stores/energyAccountStore.js.map +1 -1
- /package/{BLOCK_COVERAGE_AUDIT.md → brightchain-lib/BLOCK_COVERAGE_AUDIT.md} +0 -0
- /package/{BROWSER_COMPAT.md → brightchain-lib/BROWSER_COMPAT.md} +0 -0
- /package/{DEPRECATIONS.md → brightchain-lib/DEPRECATIONS.md} +0 -0
- /package/{DEPRECATIONS_REMOVED.md → brightchain-lib/DEPRECATIONS_REMOVED.md} +0 -0
- /package/{MIGRATION.md → brightchain-lib/MIGRATION.md} +0 -0
- /package/{NAMING_AUDIT.md → brightchain-lib/NAMING_AUDIT.md} +0 -0
- /package/{NAMING_CONVENTIONS.md → brightchain-lib/NAMING_CONVENTIONS.md} +0 -0
- /package/{README.md → brightchain-lib/README.md} +0 -0
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview QuorumStateMachine implementation.
|
|
4
|
+
*
|
|
5
|
+
* Central coordinator for the quorum system. Manages operational mode,
|
|
6
|
+
* epoch lifecycle, proposal/vote orchestration, and delegates to
|
|
7
|
+
* SealingService for cryptographic operations.
|
|
8
|
+
*
|
|
9
|
+
* @see Requirements 1-8, 10-13
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.QuorumStateMachine = void 0;
|
|
13
|
+
const ecies_lib_1 = require("@digitaldefiance/ecies-lib");
|
|
14
|
+
const uuid_1 = require("uuid");
|
|
15
|
+
const proposalActionType_1 = require("../enumerations/proposalActionType");
|
|
16
|
+
const proposalStatus_1 = require("../enumerations/proposalStatus");
|
|
17
|
+
const quorumErrorType_1 = require("../enumerations/quorumErrorType");
|
|
18
|
+
const quorumOperationalMode_1 = require("../enumerations/quorumOperationalMode");
|
|
19
|
+
const quorumError_1 = require("../errors/quorumError");
|
|
20
|
+
const quorumDataRecord_1 = require("../quorumDataRecord");
|
|
21
|
+
const checksum_service_1 = require("./checksum.service");
|
|
22
|
+
/**
|
|
23
|
+
* Constant-time comparison of two Uint8Array buffers.
|
|
24
|
+
* Prevents timing side-channel attacks by always comparing all bytes.
|
|
25
|
+
*/
|
|
26
|
+
function constantTimeEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
let result = 0;
|
|
31
|
+
for (let i = 0; i < a.length; i++) {
|
|
32
|
+
result |= a[i] ^ b[i];
|
|
33
|
+
}
|
|
34
|
+
return result === 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* QuorumStateMachine is the central coordinator for the quorum system.
|
|
38
|
+
*
|
|
39
|
+
* It manages:
|
|
40
|
+
* - Operational mode (Bootstrap / Quorum / TransitionInProgress)
|
|
41
|
+
* - Epoch lifecycle
|
|
42
|
+
* - Document sealing/unsealing with mode-aware delegation
|
|
43
|
+
* - Metrics collection
|
|
44
|
+
*
|
|
45
|
+
* @template TID - Platform ID type for frontend/backend DTO compatibility
|
|
46
|
+
*/
|
|
47
|
+
class QuorumStateMachine {
|
|
48
|
+
constructor(db, sealingService, gossipService, auditLogService, aliasRegistry) {
|
|
49
|
+
this.db = db;
|
|
50
|
+
this.sealingService = sealingService;
|
|
51
|
+
this.gossipService = gossipService;
|
|
52
|
+
this.auditLogService = auditLogService;
|
|
53
|
+
this.aliasRegistry = aliasRegistry;
|
|
54
|
+
this.mode = null;
|
|
55
|
+
this.currentEpochData = null;
|
|
56
|
+
this.configuredThreshold = 0;
|
|
57
|
+
this.initialized = false;
|
|
58
|
+
this.checksumService = new checksum_service_1.ChecksumService();
|
|
59
|
+
// Metrics tracking
|
|
60
|
+
this.metricsData = {
|
|
61
|
+
proposalsTotal: 0,
|
|
62
|
+
proposalsPending: 0,
|
|
63
|
+
votesLatencyMs: 0,
|
|
64
|
+
redistributionProgress: -1,
|
|
65
|
+
redistributionFailures: 0,
|
|
66
|
+
expirationLastRun: null,
|
|
67
|
+
expirationDeletedTotal: 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Initialize the quorum system.
|
|
72
|
+
*
|
|
73
|
+
* - Checks for persisted operational state first (restore on restart).
|
|
74
|
+
* - If TransitionInProgress is detected, triggers rollback recovery.
|
|
75
|
+
* - Otherwise, determines mode based on member count vs threshold.
|
|
76
|
+
* - Persists the operational state and creates epoch 1.
|
|
77
|
+
*/
|
|
78
|
+
async initialize(members, threshold) {
|
|
79
|
+
this.configuredThreshold = threshold;
|
|
80
|
+
// Check for persisted state (restart recovery)
|
|
81
|
+
const persistedState = await this.db.getOperationalState();
|
|
82
|
+
if (persistedState) {
|
|
83
|
+
// Crash recovery: detect TransitionInProgress and rollback
|
|
84
|
+
if (persistedState.mode === quorumOperationalMode_1.QuorumOperationalMode.TransitionInProgress) {
|
|
85
|
+
// Get journal entries for the current epoch to restore documents
|
|
86
|
+
const journalEntries = await this.db.getJournalEntries(persistedState.currentEpochNumber);
|
|
87
|
+
// Roll back already-re-split documents from journal
|
|
88
|
+
for (const entry of journalEntries) {
|
|
89
|
+
const doc = await this.db.getDocument(entry.documentId);
|
|
90
|
+
if (!doc) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Restore the document with old shares/members/threshold
|
|
94
|
+
const restoredDoc = new quorumDataRecord_1.QuorumDataRecord(doc.creator, entry.oldMemberIds.map((id) => doc.enhancedProvider.fromBytes(new Uint8Array(Buffer.from(id, 'hex')))), entry.oldThreshold, doc.encryptedData, entry.oldShares, doc.enhancedProvider, doc.checksum, doc.signature, doc.id, doc.dateCreated, new Date(), undefined, entry.oldThreshold < 2, entry.oldEpoch, doc.sealedUnderBootstrap, doc.identityRecoveryRecordId);
|
|
95
|
+
await this.db.saveDocument(restoredDoc);
|
|
96
|
+
}
|
|
97
|
+
// Delete journal entries and reset to Bootstrap
|
|
98
|
+
if (journalEntries.length > 0) {
|
|
99
|
+
await this.db.deleteJournalEntries(persistedState.currentEpochNumber);
|
|
100
|
+
}
|
|
101
|
+
// Reset operational state to Bootstrap
|
|
102
|
+
const bootstrapState = {
|
|
103
|
+
mode: quorumOperationalMode_1.QuorumOperationalMode.Bootstrap,
|
|
104
|
+
currentEpochNumber: persistedState.currentEpochNumber,
|
|
105
|
+
lastUpdated: new Date(),
|
|
106
|
+
};
|
|
107
|
+
await this.db.saveOperationalState(bootstrapState);
|
|
108
|
+
}
|
|
109
|
+
// Restore from persisted state
|
|
110
|
+
const restoredEpoch = await this.db.getEpoch(persistedState.currentEpochNumber);
|
|
111
|
+
if (restoredEpoch) {
|
|
112
|
+
this.mode =
|
|
113
|
+
persistedState.mode === quorumOperationalMode_1.QuorumOperationalMode.TransitionInProgress
|
|
114
|
+
? quorumOperationalMode_1.QuorumOperationalMode.Bootstrap
|
|
115
|
+
: persistedState.mode;
|
|
116
|
+
this.currentEpochData = restoredEpoch;
|
|
117
|
+
this.initialized = true;
|
|
118
|
+
return restoredEpoch;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Fresh initialization: determine mode based on member count vs threshold
|
|
122
|
+
const memberCount = members.length;
|
|
123
|
+
const isBootstrap = memberCount < threshold;
|
|
124
|
+
this.mode = isBootstrap
|
|
125
|
+
? quorumOperationalMode_1.QuorumOperationalMode.Bootstrap
|
|
126
|
+
: quorumOperationalMode_1.QuorumOperationalMode.Quorum;
|
|
127
|
+
const effectiveThreshold = isBootstrap
|
|
128
|
+
? Math.max(memberCount, 1)
|
|
129
|
+
: threshold;
|
|
130
|
+
// Save members to database
|
|
131
|
+
for (const member of members) {
|
|
132
|
+
const hexId = (0, ecies_lib_1.uint8ArrayToHex)(member.idBytes);
|
|
133
|
+
const quorumMember = {
|
|
134
|
+
id: hexId,
|
|
135
|
+
publicKey: member.publicKey,
|
|
136
|
+
metadata: { name: `Member-${hexId.substring(0, 8)}` },
|
|
137
|
+
isActive: true,
|
|
138
|
+
createdAt: new Date(),
|
|
139
|
+
updatedAt: new Date(),
|
|
140
|
+
};
|
|
141
|
+
await this.db.saveMember(quorumMember);
|
|
142
|
+
}
|
|
143
|
+
// Create epoch 1
|
|
144
|
+
const memberIds = members.map((m) => (0, ecies_lib_1.uint8ArrayToHex)(m.idBytes));
|
|
145
|
+
const epoch = {
|
|
146
|
+
epochNumber: 1,
|
|
147
|
+
memberIds,
|
|
148
|
+
threshold: effectiveThreshold,
|
|
149
|
+
mode: this.mode,
|
|
150
|
+
createdAt: new Date(),
|
|
151
|
+
};
|
|
152
|
+
await this.db.saveEpoch(epoch);
|
|
153
|
+
// Persist operational state
|
|
154
|
+
const opState = {
|
|
155
|
+
mode: this.mode,
|
|
156
|
+
currentEpochNumber: 1,
|
|
157
|
+
lastUpdated: new Date(),
|
|
158
|
+
};
|
|
159
|
+
await this.db.saveOperationalState(opState);
|
|
160
|
+
this.currentEpochData = epoch;
|
|
161
|
+
this.initialized = true;
|
|
162
|
+
return epoch;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Rollback a transition that was interrupted (crash recovery or failure).
|
|
166
|
+
*
|
|
167
|
+
* For each journal entry, restores the document's old shares, member IDs,
|
|
168
|
+
* and threshold from the journal. Then deletes all journal entries and
|
|
169
|
+
* resets operational state to Bootstrap mode.
|
|
170
|
+
*
|
|
171
|
+
* @param epochNumber - The epoch number to rollback
|
|
172
|
+
* @param emitAudit - Whether to emit a transition_ceremony_failed audit entry (false during crash recovery in initialize)
|
|
173
|
+
*/
|
|
174
|
+
async rollbackTransition(epochNumber, emitAudit = false) {
|
|
175
|
+
const journalEntries = await this.db.getJournalEntries(epochNumber);
|
|
176
|
+
// Restore each document from its journal entry
|
|
177
|
+
for (const entry of journalEntries) {
|
|
178
|
+
const doc = await this.db.getDocument(entry.documentId);
|
|
179
|
+
if (!doc) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Reconstruct the document with old shares/members/threshold
|
|
183
|
+
const restoredDoc = new quorumDataRecord_1.QuorumDataRecord(doc.creator, entry.oldMemberIds.map((id) => doc.enhancedProvider.fromBytes(new Uint8Array(Buffer.from(id, 'hex')))), entry.oldThreshold, doc.encryptedData, entry.oldShares, doc.enhancedProvider, doc.checksum, doc.signature, doc.id, doc.dateCreated, new Date(), undefined, entry.oldThreshold < 2, // bootstrapMode if threshold < 2
|
|
184
|
+
entry.oldEpoch, doc.sealedUnderBootstrap, doc.identityRecoveryRecordId);
|
|
185
|
+
await this.db.saveDocument(restoredDoc);
|
|
186
|
+
}
|
|
187
|
+
// Delete all journal entries for this epoch
|
|
188
|
+
if (journalEntries.length > 0) {
|
|
189
|
+
await this.db.deleteJournalEntries(epochNumber);
|
|
190
|
+
}
|
|
191
|
+
// Reset operational state to Bootstrap
|
|
192
|
+
const opState = {
|
|
193
|
+
mode: quorumOperationalMode_1.QuorumOperationalMode.Bootstrap,
|
|
194
|
+
currentEpochNumber: epochNumber,
|
|
195
|
+
lastUpdated: new Date(),
|
|
196
|
+
};
|
|
197
|
+
await this.db.saveOperationalState(opState);
|
|
198
|
+
if (emitAudit) {
|
|
199
|
+
await this.emitAuditEntry('transition_ceremony_failed', {
|
|
200
|
+
epochNumber,
|
|
201
|
+
rolledBackDocuments: journalEntries.length,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Ensure the state machine is initialized before operations. */
|
|
206
|
+
ensureInitialized() {
|
|
207
|
+
if (!this.initialized) {
|
|
208
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.Uninitialized);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Ensure operations are not blocked by a transition ceremony. */
|
|
212
|
+
ensureNotTransitioning() {
|
|
213
|
+
if (this.mode === quorumOperationalMode_1.QuorumOperationalMode.TransitionInProgress) {
|
|
214
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.TransitionInProgress);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Create the next epoch with incremented epoch number.
|
|
219
|
+
*
|
|
220
|
+
* 1. Gets the current epoch number
|
|
221
|
+
* 2. Creates a new epoch with epochNumber = current + 1
|
|
222
|
+
* 3. Saves the epoch to the database
|
|
223
|
+
* 4. Updates the operational state with the new epoch number
|
|
224
|
+
* 5. Updates the cached currentEpochData
|
|
225
|
+
* 6. Returns the new epoch
|
|
226
|
+
*
|
|
227
|
+
* @param memberIds - Active member IDs for the new epoch
|
|
228
|
+
* @param threshold - Threshold for the new epoch
|
|
229
|
+
* @param mode - Operational mode for the new epoch
|
|
230
|
+
* @param previousEpochNumber - Optional override for the previous epoch number
|
|
231
|
+
* @returns The newly created QuorumEpoch
|
|
232
|
+
*/
|
|
233
|
+
async createNextEpoch(memberIds, threshold, mode, previousEpochNumber) {
|
|
234
|
+
const currentEpochNum = previousEpochNumber ?? this.currentEpochData?.epochNumber ?? 0;
|
|
235
|
+
const nextEpochNumber = currentEpochNum + 1;
|
|
236
|
+
const epoch = {
|
|
237
|
+
epochNumber: nextEpochNumber,
|
|
238
|
+
memberIds,
|
|
239
|
+
threshold,
|
|
240
|
+
mode,
|
|
241
|
+
createdAt: new Date(),
|
|
242
|
+
previousEpochNumber: currentEpochNum > 0 ? currentEpochNum : undefined,
|
|
243
|
+
};
|
|
244
|
+
await this.db.saveEpoch(epoch);
|
|
245
|
+
// Update operational state
|
|
246
|
+
const opState = {
|
|
247
|
+
mode,
|
|
248
|
+
currentEpochNumber: nextEpochNumber,
|
|
249
|
+
lastUpdated: new Date(),
|
|
250
|
+
};
|
|
251
|
+
await this.db.saveOperationalState(opState);
|
|
252
|
+
// Update cached state
|
|
253
|
+
this.currentEpochData = epoch;
|
|
254
|
+
this.mode = mode;
|
|
255
|
+
return epoch;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Emit an audit log entry.
|
|
259
|
+
*
|
|
260
|
+
* If an AuditLogService is configured, routes through it for chain linking
|
|
261
|
+
* and signing. Otherwise, writes directly to the database.
|
|
262
|
+
*
|
|
263
|
+
* @param eventType - The type of audit event
|
|
264
|
+
* @param details - Additional event-specific details
|
|
265
|
+
* @param targetMemberId - Optional target member ID
|
|
266
|
+
*/
|
|
267
|
+
async emitAuditEntry(eventType, details, targetMemberId) {
|
|
268
|
+
const entry = {
|
|
269
|
+
id: (0, uuid_1.v4)(),
|
|
270
|
+
eventType,
|
|
271
|
+
targetMemberId,
|
|
272
|
+
details,
|
|
273
|
+
timestamp: new Date(),
|
|
274
|
+
};
|
|
275
|
+
if (this.auditLogService) {
|
|
276
|
+
await this.auditLogService.appendEntry(entry);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
await this.db.appendAuditEntry(entry);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Resolve IQuorumMember records into Member objects suitable for SealingService.
|
|
284
|
+
*
|
|
285
|
+
* Creates Member instances with only public keys (no private keys) from
|
|
286
|
+
* the database member records. These are sufficient for encrypting new shares.
|
|
287
|
+
*
|
|
288
|
+
* @param quorumMembers - Database member records to resolve
|
|
289
|
+
* @returns Array of Member objects with public keys
|
|
290
|
+
*/
|
|
291
|
+
resolveMembersFromRecords(quorumMembers) {
|
|
292
|
+
return quorumMembers.map((qm) => {
|
|
293
|
+
return new ecies_lib_1.Member(this.sealingService.eciesServiceRef, ecies_lib_1.MemberType.User, qm.metadata.name || `Member-${qm.id.substring(0, 8)}`, new ecies_lib_1.EmailString(`${qm.id.substring(0, 8)}@quorum.local`), qm.publicKey, undefined, // no private key
|
|
294
|
+
undefined, // no wallet
|
|
295
|
+
this.sealingService.enhancedProviderRef.fromBytes(new Uint8Array(Buffer.from(qm.id, 'hex'))));
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Perform batched share redistribution for all documents in a given epoch.
|
|
300
|
+
*
|
|
301
|
+
* Processes documents in pages using `listDocumentsByEpoch`, decrypts shares
|
|
302
|
+
* from threshold members, calls `redistributeShares`, updates documents,
|
|
303
|
+
* and saves them back to the database.
|
|
304
|
+
*
|
|
305
|
+
* @param fromEpochNumber - The epoch whose documents need redistribution
|
|
306
|
+
* @param thresholdMembers - Members with private keys to decrypt existing shares
|
|
307
|
+
* @param newMembers - The new member set for re-encryption
|
|
308
|
+
* @param newThreshold - The new threshold for shares
|
|
309
|
+
* @param oldSharingConfig - The old sharing configuration (totalShares, threshold)
|
|
310
|
+
* @param config - Redistribution configuration (batch size, progress, continue-on-failure)
|
|
311
|
+
* @returns Array of failed document IDs
|
|
312
|
+
*/
|
|
313
|
+
async redistributeDocuments(fromEpochNumber, thresholdMembers, newMembers, newThreshold, oldSharingConfig, config) {
|
|
314
|
+
const failedDocIds = [];
|
|
315
|
+
let page = 0;
|
|
316
|
+
let processed = 0;
|
|
317
|
+
let totalEstimate = 0;
|
|
318
|
+
const pageSize = config.batchSize;
|
|
319
|
+
// Process documents page by page
|
|
320
|
+
let docs;
|
|
321
|
+
do {
|
|
322
|
+
docs = await this.db.listDocumentsByEpoch(fromEpochNumber, page, pageSize);
|
|
323
|
+
if (page === 0 && docs.length === pageSize) {
|
|
324
|
+
// Rough estimate: at least one more page
|
|
325
|
+
totalEstimate = pageSize * 2;
|
|
326
|
+
}
|
|
327
|
+
else if (page === 0) {
|
|
328
|
+
totalEstimate = docs.length;
|
|
329
|
+
}
|
|
330
|
+
for (const doc of docs) {
|
|
331
|
+
const docHexId = (0, ecies_lib_1.uint8ArrayToHex)(doc.enhancedProvider.toBytes(doc.id));
|
|
332
|
+
try {
|
|
333
|
+
// Decrypt shares from threshold members
|
|
334
|
+
const decryptedShares = await this.sealingService.decryptShares(doc, thresholdMembers);
|
|
335
|
+
// Build a map of memberId -> decrypted share hex
|
|
336
|
+
const sharesMap = new Map();
|
|
337
|
+
for (let i = 0; i < thresholdMembers.length; i++) {
|
|
338
|
+
const memberId = (0, ecies_lib_1.uint8ArrayToHex)(thresholdMembers[i].idBytes);
|
|
339
|
+
sharesMap.set(memberId, decryptedShares[i]);
|
|
340
|
+
}
|
|
341
|
+
// Redistribute shares to new members
|
|
342
|
+
const newEncryptedShares = await this.sealingService.redistributeShares(sharesMap, newMembers, newThreshold, oldSharingConfig);
|
|
343
|
+
// Create updated document with new shares, memberIDs, and threshold
|
|
344
|
+
const newMemberTIDs = newMembers.map((m) => m.id);
|
|
345
|
+
const updatedDoc = new quorumDataRecord_1.QuorumDataRecord(doc.creator, newMemberTIDs, newThreshold, doc.encryptedData, newEncryptedShares, doc.enhancedProvider, doc.checksum, doc.signature, doc.id, doc.dateCreated, new Date(), undefined, newThreshold < 2, // bootstrapMode if threshold < 2
|
|
346
|
+
this.currentEpochData?.epochNumber ?? doc.epochNumber, doc.sealedUnderBootstrap, doc.identityRecoveryRecordId);
|
|
347
|
+
await this.db.saveDocument(updatedDoc);
|
|
348
|
+
processed++;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
failedDocIds.push(docHexId);
|
|
352
|
+
if (!config.continueOnFailure) {
|
|
353
|
+
this.metricsData.redistributionFailures += failedDocIds.length;
|
|
354
|
+
return failedDocIds;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Report progress
|
|
358
|
+
if (config.onProgress) {
|
|
359
|
+
config.onProgress(processed, totalEstimate, failedDocIds);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
page++;
|
|
363
|
+
} while (docs.length === pageSize);
|
|
364
|
+
// Update metrics
|
|
365
|
+
this.metricsData.redistributionProgress =
|
|
366
|
+
totalEstimate > 0 ? processed / totalEstimate : 1;
|
|
367
|
+
this.metricsData.redistributionFailures += failedDocIds.length;
|
|
368
|
+
return failedDocIds;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get the current operational mode.
|
|
372
|
+
* Restores from persisted state on first call if needed.
|
|
373
|
+
*/
|
|
374
|
+
async getMode() {
|
|
375
|
+
if (this.mode !== null) {
|
|
376
|
+
return this.mode;
|
|
377
|
+
}
|
|
378
|
+
// Try to restore from persisted state
|
|
379
|
+
const persistedState = await this.db.getOperationalState();
|
|
380
|
+
if (persistedState) {
|
|
381
|
+
this.mode = persistedState.mode;
|
|
382
|
+
return this.mode;
|
|
383
|
+
}
|
|
384
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.Uninitialized);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Initiate a transition ceremony from Bootstrap to Quorum mode.
|
|
388
|
+
*
|
|
389
|
+
* Flow:
|
|
390
|
+
* 1. Verify members >= threshold, set mode to TransitionInProgress
|
|
391
|
+
* 2. Emit transition_ceremony_started audit entry
|
|
392
|
+
* 3. Block seal/unseal/submitProposal (via ensureNotTransitioning)
|
|
393
|
+
* 4. For each bootstrap-sealed document (paginated):
|
|
394
|
+
* a. Save journal entry with old shares/members/threshold/epoch
|
|
395
|
+
* b. Redistribute shares to full member set with configured threshold
|
|
396
|
+
* c. Update document in database
|
|
397
|
+
* 5. On success: create new epoch in Quorum mode, delete journal entries,
|
|
398
|
+
* emit transition_ceremony_completed, unblock
|
|
399
|
+
* 6. On failure: rollback from journal, reset to Bootstrap, delete journal
|
|
400
|
+
* entries, emit transition_ceremony_failed
|
|
401
|
+
*/
|
|
402
|
+
async initiateTransition() {
|
|
403
|
+
this.ensureInitialized();
|
|
404
|
+
if (this.mode !== quorumOperationalMode_1.QuorumOperationalMode.Bootstrap) {
|
|
405
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.InvalidModeTransition);
|
|
406
|
+
}
|
|
407
|
+
const activeMembers = await this.db.listActiveMembers();
|
|
408
|
+
if (activeMembers.length < this.configuredThreshold) {
|
|
409
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.InsufficientMembersForTransition);
|
|
410
|
+
}
|
|
411
|
+
const currentEpochNumber = this.currentEpochData?.epochNumber ?? 1;
|
|
412
|
+
const previousThreshold = this.currentEpochData?.threshold ?? 1;
|
|
413
|
+
const previousMemberIds = this.currentEpochData?.memberIds ?? [];
|
|
414
|
+
// Set mode to TransitionInProgress — blocks seal/unseal/submitProposal
|
|
415
|
+
this.mode = quorumOperationalMode_1.QuorumOperationalMode.TransitionInProgress;
|
|
416
|
+
const opState = {
|
|
417
|
+
mode: quorumOperationalMode_1.QuorumOperationalMode.TransitionInProgress,
|
|
418
|
+
currentEpochNumber,
|
|
419
|
+
lastUpdated: new Date(),
|
|
420
|
+
};
|
|
421
|
+
await this.db.saveOperationalState(opState);
|
|
422
|
+
// Emit transition_ceremony_started audit entry
|
|
423
|
+
await this.emitAuditEntry('transition_ceremony_started', {
|
|
424
|
+
epochNumber: currentEpochNumber,
|
|
425
|
+
memberCount: activeMembers.length,
|
|
426
|
+
configuredThreshold: this.configuredThreshold,
|
|
427
|
+
});
|
|
428
|
+
// Resolve members for redistribution
|
|
429
|
+
const newMemberObjects = this.resolveMembersFromRecords(activeMembers);
|
|
430
|
+
const newMemberIds = activeMembers.map((m) => m.id);
|
|
431
|
+
// Batched re-split of all bootstrap-sealed documents
|
|
432
|
+
const batchSize = 100;
|
|
433
|
+
let page = 0;
|
|
434
|
+
let docs;
|
|
435
|
+
const failedDocIds = [];
|
|
436
|
+
try {
|
|
437
|
+
// Emit share_redistribution_started
|
|
438
|
+
await this.emitAuditEntry('share_redistribution_started', {
|
|
439
|
+
reason: 'transition_ceremony',
|
|
440
|
+
fromEpoch: currentEpochNumber,
|
|
441
|
+
});
|
|
442
|
+
do {
|
|
443
|
+
docs = await this.db.listDocumentsByEpoch(currentEpochNumber, page, batchSize);
|
|
444
|
+
for (const doc of docs) {
|
|
445
|
+
const docHexId = (0, ecies_lib_1.uint8ArrayToHex)(doc.enhancedProvider.toBytes(doc.id));
|
|
446
|
+
// Save journal entry BEFORE modifying the document
|
|
447
|
+
const oldMemberHexIds = doc.memberIDs.map((mid) => (0, ecies_lib_1.uint8ArrayToHex)(doc.enhancedProvider.toBytes(mid)));
|
|
448
|
+
const journalEntry = {
|
|
449
|
+
documentId: docHexId,
|
|
450
|
+
oldShares: new Map(doc.encryptedSharesByMemberId),
|
|
451
|
+
oldMemberIds: oldMemberHexIds,
|
|
452
|
+
oldThreshold: doc.sharesRequired,
|
|
453
|
+
oldEpoch: doc.epochNumber,
|
|
454
|
+
};
|
|
455
|
+
await this.db.saveJournalEntry(journalEntry);
|
|
456
|
+
try {
|
|
457
|
+
// Decrypt shares from threshold members
|
|
458
|
+
const decryptedShares = await this.sealingService.decryptShares(doc, newMemberObjects);
|
|
459
|
+
// Build shares map
|
|
460
|
+
const sharesMap = new Map();
|
|
461
|
+
for (let i = 0; i < newMemberObjects.length; i++) {
|
|
462
|
+
const memberId = (0, ecies_lib_1.uint8ArrayToHex)(newMemberObjects[i].idBytes);
|
|
463
|
+
sharesMap.set(memberId, decryptedShares[i]);
|
|
464
|
+
}
|
|
465
|
+
// Redistribute shares to full member set with configured threshold
|
|
466
|
+
const newEncryptedShares = await this.sealingService.redistributeShares(sharesMap, newMemberObjects, this.configuredThreshold, {
|
|
467
|
+
totalShares: previousMemberIds.length,
|
|
468
|
+
threshold: previousThreshold,
|
|
469
|
+
});
|
|
470
|
+
// Create updated document with new shares
|
|
471
|
+
const newMemberTIDs = newMemberObjects.map((m) => m.id);
|
|
472
|
+
const updatedDoc = new quorumDataRecord_1.QuorumDataRecord(doc.creator, newMemberTIDs, this.configuredThreshold, doc.encryptedData, newEncryptedShares, doc.enhancedProvider, doc.checksum, doc.signature, doc.id, doc.dateCreated, new Date(), undefined, false, // no longer bootstrap mode
|
|
473
|
+
currentEpochNumber + 1, doc.sealedUnderBootstrap, doc.identityRecoveryRecordId);
|
|
474
|
+
await this.db.saveDocument(updatedDoc);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
failedDocIds.push(docHexId);
|
|
478
|
+
// On any failure, abort and rollback
|
|
479
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.RedistributionFailed);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
page++;
|
|
483
|
+
} while (docs.length === batchSize);
|
|
484
|
+
// Emit share_redistribution_completed
|
|
485
|
+
await this.emitAuditEntry('share_redistribution_completed', {
|
|
486
|
+
reason: 'transition_ceremony',
|
|
487
|
+
fromEpoch: currentEpochNumber,
|
|
488
|
+
});
|
|
489
|
+
// SUCCESS PATH: all documents re-split
|
|
490
|
+
// Create new epoch in Quorum mode
|
|
491
|
+
await this.createTransitionEpoch(newMemberIds, this.configuredThreshold);
|
|
492
|
+
// Delete journal entries (no longer needed)
|
|
493
|
+
await this.db.deleteJournalEntries(currentEpochNumber);
|
|
494
|
+
// Emit transition_ceremony_completed
|
|
495
|
+
await this.emitAuditEntry('transition_ceremony_completed', {
|
|
496
|
+
epochNumber: this.currentEpochData?.epochNumber,
|
|
497
|
+
previousEpochNumber: currentEpochNumber,
|
|
498
|
+
memberCount: newMemberIds.length,
|
|
499
|
+
threshold: this.configuredThreshold,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
// FAILURE PATH: rollback from journal
|
|
504
|
+
await this.rollbackTransition(currentEpochNumber, true);
|
|
505
|
+
// Restore in-memory state to Bootstrap
|
|
506
|
+
this.mode = quorumOperationalMode_1.QuorumOperationalMode.Bootstrap;
|
|
507
|
+
// Emit share_redistribution_failed if we got past the start
|
|
508
|
+
if (failedDocIds.length > 0) {
|
|
509
|
+
await this.emitAuditEntry('share_redistribution_failed', {
|
|
510
|
+
reason: 'transition_ceremony',
|
|
511
|
+
failedDocumentIds: failedDocIds,
|
|
512
|
+
failedCount: failedDocIds.length,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// Re-throw the error so the caller knows the ceremony failed
|
|
516
|
+
if (error instanceof quorumError_1.QuorumError) {
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.RedistributionFailed);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Create a new epoch for a completed transition ceremony.
|
|
524
|
+
*
|
|
525
|
+
* Called by the transition ceremony logic (Task 10) when all documents
|
|
526
|
+
* have been successfully re-split. Creates a new epoch in Quorum mode
|
|
527
|
+
* with the full member set and configured threshold.
|
|
528
|
+
*
|
|
529
|
+
* @param memberIds - The full member set after transition
|
|
530
|
+
* @param threshold - The configured quorum threshold
|
|
531
|
+
* @returns The new QuorumEpoch in Quorum mode
|
|
532
|
+
*/
|
|
533
|
+
async createTransitionEpoch(memberIds, threshold) {
|
|
534
|
+
return this.createNextEpoch(memberIds, threshold, quorumOperationalMode_1.QuorumOperationalMode.Quorum);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Add a new member to the quorum.
|
|
538
|
+
*
|
|
539
|
+
* Saves the member, increments the epoch, triggers batched share
|
|
540
|
+
* redistribution for all documents in the previous epoch, and emits
|
|
541
|
+
* audit entries for member_added and share_redistribution_*.
|
|
542
|
+
*
|
|
543
|
+
* @param member - The member to add
|
|
544
|
+
* @param metadata - Metadata for the member
|
|
545
|
+
* @param redistributionConfig - Optional redistribution configuration
|
|
546
|
+
* @returns The new QuorumEpoch after member addition
|
|
547
|
+
*/
|
|
548
|
+
async addMember(member, metadata, redistributionConfig) {
|
|
549
|
+
this.ensureInitialized();
|
|
550
|
+
this.ensureNotTransitioning();
|
|
551
|
+
const hexId = (0, ecies_lib_1.uint8ArrayToHex)(member.idBytes);
|
|
552
|
+
// Check if member already exists
|
|
553
|
+
const existing = await this.db.getMember(hexId);
|
|
554
|
+
if (existing && existing.isActive) {
|
|
555
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.MemberAlreadyExists);
|
|
556
|
+
}
|
|
557
|
+
// Save the new member
|
|
558
|
+
const quorumMember = {
|
|
559
|
+
id: hexId,
|
|
560
|
+
publicKey: member.publicKey,
|
|
561
|
+
metadata,
|
|
562
|
+
isActive: true,
|
|
563
|
+
createdAt: new Date(),
|
|
564
|
+
updatedAt: new Date(),
|
|
565
|
+
};
|
|
566
|
+
await this.db.saveMember(quorumMember);
|
|
567
|
+
// Capture previous epoch info before creating the new one
|
|
568
|
+
const previousEpochNumber = this.currentEpochData?.epochNumber ?? 0;
|
|
569
|
+
const previousThreshold = this.currentEpochData?.threshold ?? 1;
|
|
570
|
+
const previousMemberIds = this.currentEpochData?.memberIds ?? [];
|
|
571
|
+
// Build new member list
|
|
572
|
+
const newMemberIds = [...previousMemberIds, hexId];
|
|
573
|
+
// Determine mode and threshold for the new epoch
|
|
574
|
+
const newMode = newMemberIds.length >= this.configuredThreshold
|
|
575
|
+
? quorumOperationalMode_1.QuorumOperationalMode.Quorum
|
|
576
|
+
: quorumOperationalMode_1.QuorumOperationalMode.Bootstrap;
|
|
577
|
+
const newThreshold = newMode === quorumOperationalMode_1.QuorumOperationalMode.Bootstrap
|
|
578
|
+
? Math.max(newMemberIds.length, 1)
|
|
579
|
+
: this.configuredThreshold;
|
|
580
|
+
// Create next epoch with the updated membership
|
|
581
|
+
const newEpoch = await this.createNextEpoch(newMemberIds, newThreshold, newMode);
|
|
582
|
+
// Emit member_added audit entry
|
|
583
|
+
await this.emitAuditEntry('member_added', {
|
|
584
|
+
memberId: hexId,
|
|
585
|
+
epochNumber: newEpoch.epochNumber,
|
|
586
|
+
previousEpochNumber,
|
|
587
|
+
memberCount: newMemberIds.length,
|
|
588
|
+
}, hexId);
|
|
589
|
+
// Trigger batched share redistribution for documents in the previous epoch
|
|
590
|
+
if (previousEpochNumber > 0 && previousMemberIds.length > 0) {
|
|
591
|
+
const config = {
|
|
592
|
+
batchSize: redistributionConfig?.batchSize ?? 100,
|
|
593
|
+
continueOnFailure: redistributionConfig?.continueOnFailure ?? true,
|
|
594
|
+
onProgress: redistributionConfig?.onProgress,
|
|
595
|
+
};
|
|
596
|
+
await this.emitAuditEntry('share_redistribution_started', {
|
|
597
|
+
reason: 'member_added',
|
|
598
|
+
memberId: hexId,
|
|
599
|
+
fromEpoch: previousEpochNumber,
|
|
600
|
+
toEpoch: newEpoch.epochNumber,
|
|
601
|
+
});
|
|
602
|
+
try {
|
|
603
|
+
// Resolve active members for redistribution
|
|
604
|
+
const activeQuorumMembers = await this.db.listActiveMembers();
|
|
605
|
+
const newMemberObjects = this.resolveMembersFromRecords(activeQuorumMembers);
|
|
606
|
+
// We need threshold members with private keys to decrypt existing shares.
|
|
607
|
+
// In a real system, these would come from approved votes or key holders.
|
|
608
|
+
// For redistribution triggered by addMember, we use the members that
|
|
609
|
+
// have shares in the existing documents. The SealingService.decryptShares
|
|
610
|
+
// method handles the actual decryption using member private keys.
|
|
611
|
+
// Since we don't have private keys here, we pass the member objects
|
|
612
|
+
// and let the redistribution handle documents that have accessible shares.
|
|
613
|
+
const failedDocIds = await this.redistributeDocuments(previousEpochNumber, newMemberObjects, newMemberObjects, newThreshold, {
|
|
614
|
+
totalShares: previousMemberIds.length,
|
|
615
|
+
threshold: previousThreshold,
|
|
616
|
+
}, config);
|
|
617
|
+
if (failedDocIds.length > 0) {
|
|
618
|
+
await this.emitAuditEntry('share_redistribution_failed', {
|
|
619
|
+
reason: 'member_added',
|
|
620
|
+
memberId: hexId,
|
|
621
|
+
failedDocumentIds: failedDocIds,
|
|
622
|
+
failedCount: failedDocIds.length,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
await this.emitAuditEntry('share_redistribution_completed', {
|
|
627
|
+
reason: 'member_added',
|
|
628
|
+
memberId: hexId,
|
|
629
|
+
fromEpoch: previousEpochNumber,
|
|
630
|
+
toEpoch: newEpoch.epochNumber,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (redistributionError) {
|
|
635
|
+
await this.emitAuditEntry('share_redistribution_failed', {
|
|
636
|
+
reason: 'member_added',
|
|
637
|
+
memberId: hexId,
|
|
638
|
+
error: redistributionError instanceof Error
|
|
639
|
+
? redistributionError.message
|
|
640
|
+
: String(redistributionError),
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return newEpoch;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Remove a member from the quorum.
|
|
648
|
+
*
|
|
649
|
+
* Validates remaining count >= threshold, removes the member,
|
|
650
|
+
* increments the epoch, triggers share redistribution with fresh
|
|
651
|
+
* polynomial coefficients, and emits audit entries for member_removed
|
|
652
|
+
* and share_redistribution_*.
|
|
653
|
+
*
|
|
654
|
+
* @param memberId - ID of the member to remove
|
|
655
|
+
* @param redistributionConfig - Optional redistribution configuration
|
|
656
|
+
* @returns The new QuorumEpoch after member removal
|
|
657
|
+
* @throws QuorumError with InsufficientRemainingMembers if removal would drop below threshold
|
|
658
|
+
* @throws QuorumError with MemberNotFound if the member is not in the current epoch
|
|
659
|
+
*/
|
|
660
|
+
async removeMember(memberId, redistributionConfig) {
|
|
661
|
+
this.ensureInitialized();
|
|
662
|
+
this.ensureNotTransitioning();
|
|
663
|
+
const currentMemberIds = this.currentEpochData?.memberIds ?? [];
|
|
664
|
+
// Validate member exists in current epoch
|
|
665
|
+
if (!currentMemberIds.includes(memberId)) {
|
|
666
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.MemberNotFound);
|
|
667
|
+
}
|
|
668
|
+
// Validate remaining count >= threshold after removal
|
|
669
|
+
const remainingCount = currentMemberIds.length - 1;
|
|
670
|
+
const currentThreshold = this.currentEpochData?.threshold ?? this.configuredThreshold;
|
|
671
|
+
if (remainingCount < currentThreshold) {
|
|
672
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.InsufficientRemainingMembers);
|
|
673
|
+
}
|
|
674
|
+
// Mark the member as inactive in the database
|
|
675
|
+
const memberRecord = await this.db.getMember(memberId);
|
|
676
|
+
if (memberRecord) {
|
|
677
|
+
const updatedMember = {
|
|
678
|
+
...memberRecord,
|
|
679
|
+
isActive: false,
|
|
680
|
+
updatedAt: new Date(),
|
|
681
|
+
};
|
|
682
|
+
await this.db.saveMember(updatedMember);
|
|
683
|
+
}
|
|
684
|
+
// Capture previous epoch info before creating the new one
|
|
685
|
+
const previousEpochNumber = this.currentEpochData?.epochNumber ?? 0;
|
|
686
|
+
const previousThreshold = this.currentEpochData?.threshold ?? 1;
|
|
687
|
+
// Build new member list without the removed member
|
|
688
|
+
const newMemberIds = currentMemberIds.filter((id) => id !== memberId);
|
|
689
|
+
// Determine mode and threshold for the new epoch
|
|
690
|
+
const currentMode = this.mode ?? quorumOperationalMode_1.QuorumOperationalMode.Bootstrap;
|
|
691
|
+
const newThreshold = currentMode === quorumOperationalMode_1.QuorumOperationalMode.Bootstrap
|
|
692
|
+
? Math.max(newMemberIds.length, 1)
|
|
693
|
+
: this.configuredThreshold;
|
|
694
|
+
// Create next epoch with the updated membership
|
|
695
|
+
const newEpoch = await this.createNextEpoch(newMemberIds, newThreshold, currentMode);
|
|
696
|
+
// Emit member_removed audit entry
|
|
697
|
+
await this.emitAuditEntry('member_removed', {
|
|
698
|
+
memberId,
|
|
699
|
+
epochNumber: newEpoch.epochNumber,
|
|
700
|
+
previousEpochNumber,
|
|
701
|
+
memberCount: newMemberIds.length,
|
|
702
|
+
}, memberId);
|
|
703
|
+
// Trigger batched share redistribution with fresh polynomial coefficients
|
|
704
|
+
if (previousEpochNumber > 0 && newMemberIds.length > 0) {
|
|
705
|
+
const config = {
|
|
706
|
+
batchSize: redistributionConfig?.batchSize ?? 100,
|
|
707
|
+
continueOnFailure: redistributionConfig?.continueOnFailure ?? true,
|
|
708
|
+
onProgress: redistributionConfig?.onProgress,
|
|
709
|
+
};
|
|
710
|
+
await this.emitAuditEntry('share_redistribution_started', {
|
|
711
|
+
reason: 'member_removed',
|
|
712
|
+
memberId,
|
|
713
|
+
fromEpoch: previousEpochNumber,
|
|
714
|
+
toEpoch: newEpoch.epochNumber,
|
|
715
|
+
});
|
|
716
|
+
try {
|
|
717
|
+
// Resolve remaining active members for redistribution
|
|
718
|
+
const activeQuorumMembers = await this.db.listActiveMembers();
|
|
719
|
+
// Filter out the removed member
|
|
720
|
+
const remainingQuorumMembers = activeQuorumMembers.filter((m) => m.id !== memberId);
|
|
721
|
+
const newMemberObjects = this.resolveMembersFromRecords(remainingQuorumMembers);
|
|
722
|
+
const failedDocIds = await this.redistributeDocuments(previousEpochNumber, newMemberObjects, newMemberObjects, newThreshold, {
|
|
723
|
+
totalShares: currentMemberIds.length,
|
|
724
|
+
threshold: previousThreshold,
|
|
725
|
+
}, config);
|
|
726
|
+
if (failedDocIds.length > 0) {
|
|
727
|
+
await this.emitAuditEntry('share_redistribution_failed', {
|
|
728
|
+
reason: 'member_removed',
|
|
729
|
+
memberId,
|
|
730
|
+
failedDocumentIds: failedDocIds,
|
|
731
|
+
failedCount: failedDocIds.length,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
await this.emitAuditEntry('share_redistribution_completed', {
|
|
736
|
+
reason: 'member_removed',
|
|
737
|
+
memberId,
|
|
738
|
+
fromEpoch: previousEpochNumber,
|
|
739
|
+
toEpoch: newEpoch.epochNumber,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
catch (redistributionError) {
|
|
744
|
+
await this.emitAuditEntry('share_redistribution_failed', {
|
|
745
|
+
reason: 'member_removed',
|
|
746
|
+
memberId,
|
|
747
|
+
error: redistributionError instanceof Error
|
|
748
|
+
? redistributionError.message
|
|
749
|
+
: String(redistributionError),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return newEpoch;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Determine the required threshold for a proposal based on inner quorum routing.
|
|
757
|
+
*
|
|
758
|
+
* When member count > 20 and the current epoch has innerQuorumMemberIds,
|
|
759
|
+
* routine operations use the inner quorum size as threshold,
|
|
760
|
+
* while critical operations use the full membership threshold.
|
|
761
|
+
*
|
|
762
|
+
* @param actionType - The proposal action type
|
|
763
|
+
* @returns The required vote threshold
|
|
764
|
+
*/
|
|
765
|
+
getRequiredThreshold(actionType) {
|
|
766
|
+
if (!this.currentEpochData) {
|
|
767
|
+
return this.configuredThreshold;
|
|
768
|
+
}
|
|
769
|
+
const memberCount = this.currentEpochData.memberIds.length;
|
|
770
|
+
const innerQuorum = this.currentEpochData.innerQuorumMemberIds;
|
|
771
|
+
// Inner quorum routing: when members > 20 and inner quorum is defined
|
|
772
|
+
if (memberCount > QuorumStateMachine.INNER_QUORUM_MEMBER_THRESHOLD &&
|
|
773
|
+
innerQuorum &&
|
|
774
|
+
innerQuorum.length > 0) {
|
|
775
|
+
if (QuorumStateMachine.ROUTINE_ACTION_TYPES.has(actionType)) {
|
|
776
|
+
// Routine operations use inner quorum threshold (majority of inner quorum)
|
|
777
|
+
return Math.ceil(innerQuorum.length / 2);
|
|
778
|
+
}
|
|
779
|
+
// Critical operations use full membership threshold
|
|
780
|
+
}
|
|
781
|
+
return this.currentEpochData.threshold;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Submit a proposal for quorum voting.
|
|
785
|
+
*
|
|
786
|
+
* Validates the proposer is an active member, validates description length,
|
|
787
|
+
* validates IDENTITY_DISCLOSURE has an attachment, assigns a unique ID,
|
|
788
|
+
* stores as pending, announces via gossip, and emits audit entry.
|
|
789
|
+
*
|
|
790
|
+
* @param proposal - The proposal input
|
|
791
|
+
* @returns The created Proposal with assigned ID and status
|
|
792
|
+
* @throws QuorumError with MissingAttachment if IDENTITY_DISCLOSURE lacks attachmentCblId
|
|
793
|
+
*/
|
|
794
|
+
async submitProposal(proposal) {
|
|
795
|
+
this.ensureInitialized();
|
|
796
|
+
this.ensureNotTransitioning();
|
|
797
|
+
// Validate description length (Req 5.3)
|
|
798
|
+
if (proposal.description.length > QuorumStateMachine.MAX_DESCRIPTION_LENGTH) {
|
|
799
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.DuplicateProposal);
|
|
800
|
+
}
|
|
801
|
+
// Validate IDENTITY_DISCLOSURE requires attachment (Req 13.3)
|
|
802
|
+
if (proposal.actionType === proposalActionType_1.ProposalActionType.IDENTITY_DISCLOSURE &&
|
|
803
|
+
!proposal.attachmentCblId) {
|
|
804
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.MissingAttachment);
|
|
805
|
+
}
|
|
806
|
+
if (!this.currentEpochData) {
|
|
807
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.Uninitialized);
|
|
808
|
+
}
|
|
809
|
+
// Determine the required threshold based on inner quorum routing
|
|
810
|
+
const requiredThreshold = this.getRequiredThreshold(proposal.actionType);
|
|
811
|
+
// Assign unique ID and create the full proposal
|
|
812
|
+
const proposalId = (0, uuid_1.v4)();
|
|
813
|
+
const now = new Date();
|
|
814
|
+
const fullProposal = {
|
|
815
|
+
id: proposalId,
|
|
816
|
+
description: proposal.description,
|
|
817
|
+
actionType: proposal.actionType,
|
|
818
|
+
actionPayload: proposal.actionPayload,
|
|
819
|
+
proposerMemberId: (proposal.actionPayload['proposerMemberId'] ??
|
|
820
|
+
this.currentEpochData.memberIds[0]),
|
|
821
|
+
status: proposalStatus_1.ProposalStatus.Pending,
|
|
822
|
+
requiredThreshold,
|
|
823
|
+
expiresAt: proposal.expiresAt,
|
|
824
|
+
createdAt: now,
|
|
825
|
+
attachmentCblId: proposal.attachmentCblId,
|
|
826
|
+
epochNumber: this.currentEpochData.epochNumber,
|
|
827
|
+
};
|
|
828
|
+
// Store as pending in the database
|
|
829
|
+
await this.db.saveProposal(fullProposal);
|
|
830
|
+
// Announce via gossip
|
|
831
|
+
const gossipMetadata = {
|
|
832
|
+
proposalId: fullProposal.id,
|
|
833
|
+
description: fullProposal.description,
|
|
834
|
+
actionType: fullProposal.actionType,
|
|
835
|
+
actionPayload: JSON.stringify(fullProposal.actionPayload),
|
|
836
|
+
proposerMemberId: fullProposal.proposerMemberId,
|
|
837
|
+
expiresAt: fullProposal.expiresAt,
|
|
838
|
+
requiredThreshold: fullProposal.requiredThreshold,
|
|
839
|
+
attachmentCblId: fullProposal.attachmentCblId,
|
|
840
|
+
};
|
|
841
|
+
await this.gossipService.announceQuorumProposal(gossipMetadata);
|
|
842
|
+
// Emit audit entry
|
|
843
|
+
await this.emitAuditEntry('proposal_created', {
|
|
844
|
+
proposalId: fullProposal.id,
|
|
845
|
+
actionType: fullProposal.actionType,
|
|
846
|
+
proposerMemberId: fullProposal.proposerMemberId,
|
|
847
|
+
requiredThreshold: fullProposal.requiredThreshold,
|
|
848
|
+
epochNumber: fullProposal.epochNumber,
|
|
849
|
+
hasAttachment: !!fullProposal.attachmentCblId,
|
|
850
|
+
});
|
|
851
|
+
// Update metrics
|
|
852
|
+
this.metricsData.proposalsTotal++;
|
|
853
|
+
this.metricsData.proposalsPending++;
|
|
854
|
+
return fullProposal;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Submit a vote on a pending proposal.
|
|
858
|
+
*
|
|
859
|
+
* Validates the proposal exists and is pending, validates the voter is an
|
|
860
|
+
* active member, checks for duplicate votes, stores the vote, announces
|
|
861
|
+
* via gossip, emits audit entry, and triggers vote tallying.
|
|
862
|
+
*
|
|
863
|
+
* @param vote - The vote input
|
|
864
|
+
* @throws QuorumError with ProposalExpired if proposal is not pending
|
|
865
|
+
* @throws QuorumError with DuplicateVote if voter already voted
|
|
866
|
+
* @throws QuorumError with VoterNotOnProposal if voter is not an active member
|
|
867
|
+
*/
|
|
868
|
+
async submitVote(vote) {
|
|
869
|
+
this.ensureInitialized();
|
|
870
|
+
this.ensureNotTransitioning();
|
|
871
|
+
// Validate proposal exists
|
|
872
|
+
const proposal = await this.db.getProposal(vote.proposalId);
|
|
873
|
+
if (!proposal) {
|
|
874
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.DocumentNotFound);
|
|
875
|
+
}
|
|
876
|
+
// Check expiration before processing
|
|
877
|
+
if (new Date() > proposal.expiresAt) {
|
|
878
|
+
await this.expireProposal(proposal);
|
|
879
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.ProposalExpired);
|
|
880
|
+
}
|
|
881
|
+
// Validate proposal is still pending
|
|
882
|
+
if (proposal.status !== proposalStatus_1.ProposalStatus.Pending) {
|
|
883
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.ProposalExpired);
|
|
884
|
+
}
|
|
885
|
+
// Validate voter is an active member
|
|
886
|
+
if (!this.currentEpochData) {
|
|
887
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.Uninitialized);
|
|
888
|
+
}
|
|
889
|
+
const resolvedVoterId = vote.voterMemberId ?? this.currentEpochData.memberIds[0];
|
|
890
|
+
// Check voter is in the current epoch's member list
|
|
891
|
+
const isActiveMember = this.currentEpochData.memberIds.includes(resolvedVoterId);
|
|
892
|
+
if (!isActiveMember) {
|
|
893
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.VoterNotOnProposal);
|
|
894
|
+
}
|
|
895
|
+
// Check for duplicate votes
|
|
896
|
+
const existingVotes = await this.db.getVotesForProposal(vote.proposalId);
|
|
897
|
+
const alreadyVoted = existingVotes.some((v) => v.voterMemberId === resolvedVoterId);
|
|
898
|
+
if (alreadyVoted) {
|
|
899
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.DuplicateVote);
|
|
900
|
+
}
|
|
901
|
+
// Create and store the vote
|
|
902
|
+
const fullVote = {
|
|
903
|
+
proposalId: vote.proposalId,
|
|
904
|
+
voterMemberId: resolvedVoterId,
|
|
905
|
+
decision: vote.decision,
|
|
906
|
+
comment: vote.comment,
|
|
907
|
+
createdAt: new Date(),
|
|
908
|
+
};
|
|
909
|
+
await this.db.saveVote(fullVote);
|
|
910
|
+
// Announce via gossip
|
|
911
|
+
const gossipMetadata = {
|
|
912
|
+
proposalId: fullVote.proposalId,
|
|
913
|
+
voterMemberId: fullVote.voterMemberId,
|
|
914
|
+
decision: fullVote.decision,
|
|
915
|
+
comment: fullVote.comment,
|
|
916
|
+
};
|
|
917
|
+
await this.gossipService.announceQuorumVote(gossipMetadata);
|
|
918
|
+
// Emit audit entry
|
|
919
|
+
await this.emitAuditEntry('vote_cast', {
|
|
920
|
+
proposalId: fullVote.proposalId,
|
|
921
|
+
voterMemberId: fullVote.voterMemberId,
|
|
922
|
+
decision: fullVote.decision,
|
|
923
|
+
});
|
|
924
|
+
// Tally votes to check if threshold is reached
|
|
925
|
+
await this.tallyVotes(proposal);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Tally votes for a proposal.
|
|
929
|
+
*
|
|
930
|
+
* Counts approve and reject votes. If approve count >= threshold, marks
|
|
931
|
+
* the proposal as approved and dispatches the action. If reject count
|
|
932
|
+
* makes approval impossible, marks the proposal as rejected.
|
|
933
|
+
*
|
|
934
|
+
* @param proposal - The proposal to tally votes for
|
|
935
|
+
*/
|
|
936
|
+
async tallyVotes(proposal) {
|
|
937
|
+
const votes = await this.db.getVotesForProposal(proposal.id);
|
|
938
|
+
// Count distinct votes (duplicates already prevented by submitVote)
|
|
939
|
+
const approveCount = votes.filter((v) => v.decision === 'approve').length;
|
|
940
|
+
const rejectCount = votes.filter((v) => v.decision === 'reject').length;
|
|
941
|
+
// Check if threshold is reached for approval
|
|
942
|
+
if (approveCount >= proposal.requiredThreshold) {
|
|
943
|
+
// Mark as approved
|
|
944
|
+
const approvedProposal = {
|
|
945
|
+
...proposal,
|
|
946
|
+
status: proposalStatus_1.ProposalStatus.Approved,
|
|
947
|
+
};
|
|
948
|
+
await this.db.saveProposal(approvedProposal);
|
|
949
|
+
// Emit proposal_approved audit entry
|
|
950
|
+
await this.emitAuditEntry('proposal_approved', {
|
|
951
|
+
proposalId: proposal.id,
|
|
952
|
+
actionType: proposal.actionType,
|
|
953
|
+
approveCount,
|
|
954
|
+
rejectCount,
|
|
955
|
+
requiredThreshold: proposal.requiredThreshold,
|
|
956
|
+
});
|
|
957
|
+
// Update metrics
|
|
958
|
+
this.metricsData.proposalsPending = Math.max(0, this.metricsData.proposalsPending - 1);
|
|
959
|
+
// Execute the action
|
|
960
|
+
await this.executeProposalAction(approvedProposal);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// Check if approval is impossible (remaining possible votes can't reach threshold)
|
|
964
|
+
const totalMembers = this.currentEpochData?.memberIds.length ?? 0;
|
|
965
|
+
const totalVotesCast = approveCount + rejectCount;
|
|
966
|
+
const remainingVotes = totalMembers - totalVotesCast;
|
|
967
|
+
const maxPossibleApprovals = approveCount + remainingVotes;
|
|
968
|
+
if (maxPossibleApprovals < proposal.requiredThreshold) {
|
|
969
|
+
// Approval is impossible — mark as rejected
|
|
970
|
+
const rejectedProposal = {
|
|
971
|
+
...proposal,
|
|
972
|
+
status: proposalStatus_1.ProposalStatus.Rejected,
|
|
973
|
+
};
|
|
974
|
+
await this.db.saveProposal(rejectedProposal);
|
|
975
|
+
// Emit proposal_rejected audit entry
|
|
976
|
+
await this.emitAuditEntry('proposal_rejected', {
|
|
977
|
+
proposalId: proposal.id,
|
|
978
|
+
actionType: proposal.actionType,
|
|
979
|
+
approveCount,
|
|
980
|
+
rejectCount,
|
|
981
|
+
requiredThreshold: proposal.requiredThreshold,
|
|
982
|
+
});
|
|
983
|
+
// Update metrics
|
|
984
|
+
this.metricsData.proposalsPending = Math.max(0, this.metricsData.proposalsPending - 1);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Mark a proposal as expired and emit audit entry.
|
|
989
|
+
*
|
|
990
|
+
* @param proposal - The proposal to expire
|
|
991
|
+
*/
|
|
992
|
+
async expireProposal(proposal) {
|
|
993
|
+
if (proposal.status !== proposalStatus_1.ProposalStatus.Pending) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const expiredProposal = {
|
|
997
|
+
...proposal,
|
|
998
|
+
status: proposalStatus_1.ProposalStatus.Expired,
|
|
999
|
+
};
|
|
1000
|
+
await this.db.saveProposal(expiredProposal);
|
|
1001
|
+
await this.emitAuditEntry('proposal_expired', {
|
|
1002
|
+
proposalId: proposal.id,
|
|
1003
|
+
actionType: proposal.actionType,
|
|
1004
|
+
expiresAt: proposal.expiresAt.toISOString(),
|
|
1005
|
+
});
|
|
1006
|
+
this.metricsData.proposalsPending = Math.max(0, this.metricsData.proposalsPending - 1);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Execute the action associated with an approved proposal.
|
|
1010
|
+
*
|
|
1011
|
+
* Dispatches to the appropriate handler based on ProposalActionType.
|
|
1012
|
+
* Simple actions are fully implemented; complex ones are stubs that log.
|
|
1013
|
+
*
|
|
1014
|
+
* @param proposal - The approved proposal whose action to execute
|
|
1015
|
+
*/
|
|
1016
|
+
async executeProposalAction(proposal) {
|
|
1017
|
+
switch (proposal.actionType) {
|
|
1018
|
+
case proposalActionType_1.ProposalActionType.ADD_MEMBER:
|
|
1019
|
+
await this.executeAddMember(proposal);
|
|
1020
|
+
break;
|
|
1021
|
+
case proposalActionType_1.ProposalActionType.REMOVE_MEMBER:
|
|
1022
|
+
await this.executeRemoveMember(proposal);
|
|
1023
|
+
break;
|
|
1024
|
+
case proposalActionType_1.ProposalActionType.CHANGE_THRESHOLD:
|
|
1025
|
+
await this.executeChangeThreshold(proposal);
|
|
1026
|
+
break;
|
|
1027
|
+
case proposalActionType_1.ProposalActionType.TRANSITION_TO_QUORUM_MODE:
|
|
1028
|
+
await this.initiateTransition();
|
|
1029
|
+
break;
|
|
1030
|
+
case proposalActionType_1.ProposalActionType.UNSEAL_DOCUMENT:
|
|
1031
|
+
await this.executeUnsealDocument(proposal);
|
|
1032
|
+
break;
|
|
1033
|
+
case proposalActionType_1.ProposalActionType.IDENTITY_DISCLOSURE:
|
|
1034
|
+
await this.executeIdentityDisclosure(proposal);
|
|
1035
|
+
break;
|
|
1036
|
+
case proposalActionType_1.ProposalActionType.REGISTER_ALIAS:
|
|
1037
|
+
await this.executeRegisterAlias(proposal);
|
|
1038
|
+
break;
|
|
1039
|
+
case proposalActionType_1.ProposalActionType.DEREGISTER_ALIAS:
|
|
1040
|
+
await this.executeDeregisterAlias(proposal);
|
|
1041
|
+
break;
|
|
1042
|
+
case proposalActionType_1.ProposalActionType.EXTEND_STATUTE:
|
|
1043
|
+
await this.executeExtendStatute(proposal);
|
|
1044
|
+
break;
|
|
1045
|
+
case proposalActionType_1.ProposalActionType.CHANGE_INNER_QUORUM:
|
|
1046
|
+
await this.executeChangeInnerQuorum(proposal);
|
|
1047
|
+
break;
|
|
1048
|
+
case proposalActionType_1.ProposalActionType.CUSTOM:
|
|
1049
|
+
await this.executeCustomAction(proposal);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* 11.8.1 ADD_MEMBER — stub that calls addMember with payload data.
|
|
1055
|
+
*/
|
|
1056
|
+
async executeAddMember(proposal) {
|
|
1057
|
+
// Stub: In a full implementation, the member object would be constructed
|
|
1058
|
+
// from the proposal's actionPayload. For now, log the approval.
|
|
1059
|
+
await this.emitAuditEntry('member_added', {
|
|
1060
|
+
proposalId: proposal.id,
|
|
1061
|
+
actionType: proposal.actionType,
|
|
1062
|
+
stub: true,
|
|
1063
|
+
payload: proposal.actionPayload,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* 11.8.2 REMOVE_MEMBER — stub that calls removeMember with payload data.
|
|
1068
|
+
*/
|
|
1069
|
+
async executeRemoveMember(proposal) {
|
|
1070
|
+
// Stub: In a full implementation, removeMember would be called with
|
|
1071
|
+
// the memberId from actionPayload. For now, log the approval.
|
|
1072
|
+
await this.emitAuditEntry('member_removed', {
|
|
1073
|
+
proposalId: proposal.id,
|
|
1074
|
+
actionType: proposal.actionType,
|
|
1075
|
+
stub: true,
|
|
1076
|
+
payload: proposal.actionPayload,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* 11.8.3 CHANGE_THRESHOLD — update threshold and trigger share redistribution.
|
|
1081
|
+
*/
|
|
1082
|
+
async executeChangeThreshold(proposal) {
|
|
1083
|
+
const newThreshold = proposal.actionPayload['newThreshold'];
|
|
1084
|
+
if (typeof newThreshold !== 'number' || newThreshold < 1) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
this.configuredThreshold = newThreshold;
|
|
1088
|
+
if (this.currentEpochData) {
|
|
1089
|
+
const newEpoch = await this.createNextEpoch(this.currentEpochData.memberIds, newThreshold, this.currentEpochData.mode);
|
|
1090
|
+
await this.emitAuditEntry('epoch_created', {
|
|
1091
|
+
proposalId: proposal.id,
|
|
1092
|
+
reason: 'threshold_change',
|
|
1093
|
+
newThreshold,
|
|
1094
|
+
epochNumber: newEpoch.epochNumber,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* 11.8.5 UNSEAL_DOCUMENT — stub (collect shares, unseal).
|
|
1100
|
+
*/
|
|
1101
|
+
async executeUnsealDocument(proposal) {
|
|
1102
|
+
// Stub: In a full implementation, encrypted shares from approve votes
|
|
1103
|
+
// would be collected, decrypted, and used to unseal the document.
|
|
1104
|
+
await this.emitAuditEntry('proposal_approved', {
|
|
1105
|
+
proposalId: proposal.id,
|
|
1106
|
+
actionType: proposal.actionType,
|
|
1107
|
+
stub: true,
|
|
1108
|
+
documentId: proposal.actionPayload['documentId'],
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* 11.8.6 IDENTITY_DISCLOSURE — check statute of limitations, reject if expired/deleted.
|
|
1113
|
+
*
|
|
1114
|
+
* When the target IdentityRecoveryRecord has expired or been deleted by the
|
|
1115
|
+
* expiration scheduler, throws IdentityPermanentlyUnrecoverable (Req 17.6).
|
|
1116
|
+
*/
|
|
1117
|
+
async executeIdentityDisclosure(proposal) {
|
|
1118
|
+
const targetRecordId = proposal.actionPayload['targetRecordId'];
|
|
1119
|
+
// Check if the identity record still exists (not expired/deleted)
|
|
1120
|
+
if (targetRecordId) {
|
|
1121
|
+
const record = await this.db.getIdentityRecord(targetRecordId);
|
|
1122
|
+
if (!record) {
|
|
1123
|
+
// Identity has been permanently deleted by expiration scheduler
|
|
1124
|
+
await this.emitAuditEntry('identity_disclosure_rejected', {
|
|
1125
|
+
proposalId: proposal.id,
|
|
1126
|
+
reason: 'identity_permanently_unrecoverable',
|
|
1127
|
+
targetRecordId,
|
|
1128
|
+
});
|
|
1129
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.IdentityPermanentlyUnrecoverable);
|
|
1130
|
+
}
|
|
1131
|
+
// Check if record has expired (statute of limitations exceeded)
|
|
1132
|
+
if (new Date() > record.expiresAt) {
|
|
1133
|
+
await this.emitAuditEntry('identity_disclosure_expired', {
|
|
1134
|
+
proposalId: proposal.id,
|
|
1135
|
+
targetRecordId,
|
|
1136
|
+
expiresAt: record.expiresAt.toISOString(),
|
|
1137
|
+
});
|
|
1138
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.IdentityPermanentlyUnrecoverable);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
await this.emitAuditEntry('identity_disclosure_approved', {
|
|
1142
|
+
proposalId: proposal.id,
|
|
1143
|
+
actionType: proposal.actionType,
|
|
1144
|
+
targetMemberId: proposal.actionPayload['targetMemberId'],
|
|
1145
|
+
attachmentCblId: proposal.attachmentCblId,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* 11.8.7 REGISTER_ALIAS — delegate to AliasRegistry.registerAlias().
|
|
1150
|
+
*/
|
|
1151
|
+
async executeRegisterAlias(proposal) {
|
|
1152
|
+
const aliasName = proposal.actionPayload['aliasName'];
|
|
1153
|
+
const ownerMemberId = proposal.actionPayload['ownerMemberId'];
|
|
1154
|
+
const ownerPublicKeyHex = proposal.actionPayload['ownerPublicKey'];
|
|
1155
|
+
if (this.aliasRegistry && aliasName && ownerMemberId && ownerPublicKeyHex) {
|
|
1156
|
+
const ownerPublicKey = new Uint8Array(Buffer.from(ownerPublicKeyHex, 'hex'));
|
|
1157
|
+
await this.aliasRegistry.registerAlias(aliasName, ownerMemberId, ownerPublicKey);
|
|
1158
|
+
}
|
|
1159
|
+
await this.emitAuditEntry('alias_registered', {
|
|
1160
|
+
proposalId: proposal.id,
|
|
1161
|
+
actionType: proposal.actionType,
|
|
1162
|
+
aliasName: aliasName ?? proposal.actionPayload['aliasName'],
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* 11.8.8 DEREGISTER_ALIAS — delegate to AliasRegistry.deregisterAlias().
|
|
1167
|
+
*/
|
|
1168
|
+
async executeDeregisterAlias(proposal) {
|
|
1169
|
+
const aliasName = proposal.actionPayload['aliasName'];
|
|
1170
|
+
if (this.aliasRegistry && aliasName) {
|
|
1171
|
+
await this.aliasRegistry.deregisterAlias(aliasName);
|
|
1172
|
+
}
|
|
1173
|
+
await this.emitAuditEntry('alias_deregistered', {
|
|
1174
|
+
proposalId: proposal.id,
|
|
1175
|
+
actionType: proposal.actionType,
|
|
1176
|
+
aliasName: aliasName ?? proposal.actionPayload['aliasName'],
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* 11.8.9 EXTEND_STATUTE — update expiresAt on the target IdentityRecoveryRecord.
|
|
1181
|
+
*/
|
|
1182
|
+
async executeExtendStatute(proposal) {
|
|
1183
|
+
const targetRecordId = proposal.actionPayload['targetRecordId'];
|
|
1184
|
+
const newExpiresAt = proposal.actionPayload['newExpiresAt'];
|
|
1185
|
+
if (targetRecordId && newExpiresAt) {
|
|
1186
|
+
const record = await this.db.getIdentityRecord(targetRecordId);
|
|
1187
|
+
if (record) {
|
|
1188
|
+
const updatedRecord = {
|
|
1189
|
+
...record,
|
|
1190
|
+
expiresAt: new Date(newExpiresAt),
|
|
1191
|
+
};
|
|
1192
|
+
await this.db.saveIdentityRecord(updatedRecord);
|
|
1193
|
+
await this.emitAuditEntry('proposal_approved', {
|
|
1194
|
+
proposalId: proposal.id,
|
|
1195
|
+
actionType: proposal.actionType,
|
|
1196
|
+
targetRecordId,
|
|
1197
|
+
newExpiresAt,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* 11.8.10 CHANGE_INNER_QUORUM — update innerQuorumMemberIds on current epoch.
|
|
1204
|
+
*/
|
|
1205
|
+
async executeChangeInnerQuorum(proposal) {
|
|
1206
|
+
const innerQuorumMemberIds = proposal.actionPayload['innerQuorumMemberIds'];
|
|
1207
|
+
if (this.currentEpochData && innerQuorumMemberIds) {
|
|
1208
|
+
// Create a new epoch with the updated inner quorum
|
|
1209
|
+
const newEpoch = await this.createNextEpoch(this.currentEpochData.memberIds, this.currentEpochData.threshold, this.currentEpochData.mode);
|
|
1210
|
+
// Update the epoch with inner quorum member IDs
|
|
1211
|
+
newEpoch.innerQuorumMemberIds = innerQuorumMemberIds;
|
|
1212
|
+
await this.db.saveEpoch(newEpoch);
|
|
1213
|
+
await this.emitAuditEntry('epoch_created', {
|
|
1214
|
+
proposalId: proposal.id,
|
|
1215
|
+
reason: 'inner_quorum_change',
|
|
1216
|
+
innerQuorumMemberIds,
|
|
1217
|
+
epochNumber: newEpoch.epochNumber,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* 11.8.11 CUSTOM — log approval without automated execution.
|
|
1223
|
+
*/
|
|
1224
|
+
async executeCustomAction(proposal) {
|
|
1225
|
+
// Custom actions are logged but not automatically executed
|
|
1226
|
+
await this.emitAuditEntry('proposal_approved', {
|
|
1227
|
+
proposalId: proposal.id,
|
|
1228
|
+
actionType: proposal.actionType,
|
|
1229
|
+
custom: true,
|
|
1230
|
+
payload: proposal.actionPayload,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Get a proposal by its ID.
|
|
1235
|
+
*/
|
|
1236
|
+
async getProposal(proposalId) {
|
|
1237
|
+
this.ensureInitialized();
|
|
1238
|
+
return this.db.getProposal(proposalId);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Seal a document using the appropriate mode (bootstrap or quorum).
|
|
1242
|
+
*
|
|
1243
|
+
* In bootstrap mode: delegates to quorumSealBootstrap with effective threshold = member count.
|
|
1244
|
+
* In quorum mode: delegates to quorumSeal with configured threshold.
|
|
1245
|
+
* Tags document with epoch number and sealedUnderBootstrap flag.
|
|
1246
|
+
*
|
|
1247
|
+
* @throws QuorumError with TransitionInProgress if a transition ceremony is active
|
|
1248
|
+
*/
|
|
1249
|
+
async sealDocument(agent, document, memberIds, sharesRequired) {
|
|
1250
|
+
this.ensureInitialized();
|
|
1251
|
+
this.ensureNotTransitioning();
|
|
1252
|
+
if (!this.currentEpochData) {
|
|
1253
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.Uninitialized);
|
|
1254
|
+
}
|
|
1255
|
+
const isBootstrap = this.mode === quorumOperationalMode_1.QuorumOperationalMode.Bootstrap;
|
|
1256
|
+
// Resolve members from the active member list
|
|
1257
|
+
const activeMembers = await this.db.listActiveMembers();
|
|
1258
|
+
const memberMap = new Map();
|
|
1259
|
+
for (const m of activeMembers) {
|
|
1260
|
+
memberMap.set(m.id, m);
|
|
1261
|
+
}
|
|
1262
|
+
// Validate all requested member IDs exist
|
|
1263
|
+
for (const mid of memberIds) {
|
|
1264
|
+
if (!memberMap.has(mid)) {
|
|
1265
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.MemberNotFound);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
// Determine effective threshold
|
|
1269
|
+
const effectiveThreshold = isBootstrap
|
|
1270
|
+
? Math.max(memberIds.length, 1)
|
|
1271
|
+
: (sharesRequired ?? this.currentEpochData.threshold);
|
|
1272
|
+
// Delegate to SealingService
|
|
1273
|
+
let sealedDoc;
|
|
1274
|
+
if (isBootstrap) {
|
|
1275
|
+
sealedDoc = await this.sealingService.quorumSealBootstrap(agent, document, [agent], effectiveThreshold);
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
sealedDoc = await this.sealingService.quorumSeal(agent, document, [agent], effectiveThreshold);
|
|
1279
|
+
}
|
|
1280
|
+
const docHexId = (0, ecies_lib_1.uint8ArrayToHex)(sealedDoc.enhancedProvider.toBytes(sealedDoc.id));
|
|
1281
|
+
// Save to database
|
|
1282
|
+
await this.db.saveDocument(sealedDoc);
|
|
1283
|
+
return {
|
|
1284
|
+
documentId: docHexId,
|
|
1285
|
+
encryptedData: sealedDoc.encryptedData,
|
|
1286
|
+
memberIds,
|
|
1287
|
+
sharesRequired: sealedDoc.sharesRequired,
|
|
1288
|
+
createdAt: sealedDoc.dateCreated,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Unseal a document.
|
|
1293
|
+
*
|
|
1294
|
+
* Verifies checksum (SHA-3) and creator signature using constant-time
|
|
1295
|
+
* comparison before delegating to SealingService.
|
|
1296
|
+
* Returns generic error on failure without revealing which check failed (Req 8.4-8.6).
|
|
1297
|
+
*/
|
|
1298
|
+
async unsealDocument(documentId, membersWithPrivateKey) {
|
|
1299
|
+
this.ensureInitialized();
|
|
1300
|
+
this.ensureNotTransitioning();
|
|
1301
|
+
const doc = await this.db.getDocument(documentId);
|
|
1302
|
+
if (!doc) {
|
|
1303
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.DocumentNotFound);
|
|
1304
|
+
}
|
|
1305
|
+
// Verify checksum using constant-time comparison (Req 8.5)
|
|
1306
|
+
const calculatedChecksum = this.checksumService.calculateChecksum(doc.encryptedData);
|
|
1307
|
+
const checksumValid = constantTimeEqual(calculatedChecksum.toUint8Array(), doc.checksum.toUint8Array());
|
|
1308
|
+
// Verify creator signature using constant-time comparison (Req 8.5)
|
|
1309
|
+
let signatureValid = false;
|
|
1310
|
+
try {
|
|
1311
|
+
const expectedSignature = doc.creator.sign(doc.checksum.toUint8Array());
|
|
1312
|
+
signatureValid = constantTimeEqual(doc.signature, expectedSignature);
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
signatureValid = false;
|
|
1316
|
+
}
|
|
1317
|
+
// Return generic error without revealing which check failed (Req 8.6)
|
|
1318
|
+
if (!checksumValid || !signatureValid) {
|
|
1319
|
+
throw new quorumError_1.QuorumError(quorumErrorType_1.QuorumErrorType.UnableToRestoreDocument);
|
|
1320
|
+
}
|
|
1321
|
+
// Delegate to SealingService for actual decryption
|
|
1322
|
+
return this.sealingService.quorumUnseal(doc, membersWithPrivateKey);
|
|
1323
|
+
}
|
|
1324
|
+
/** Get the current epoch. */
|
|
1325
|
+
async getCurrentEpoch() {
|
|
1326
|
+
this.ensureInitialized();
|
|
1327
|
+
if (this.currentEpochData) {
|
|
1328
|
+
return this.currentEpochData;
|
|
1329
|
+
}
|
|
1330
|
+
const epoch = await this.db.getCurrentEpoch();
|
|
1331
|
+
this.currentEpochData = epoch;
|
|
1332
|
+
return epoch;
|
|
1333
|
+
}
|
|
1334
|
+
/** Get a specific epoch by number. */
|
|
1335
|
+
async getEpoch(epochNumber) {
|
|
1336
|
+
this.ensureInitialized();
|
|
1337
|
+
return this.db.getEpoch(epochNumber);
|
|
1338
|
+
}
|
|
1339
|
+
/** Get quorum system metrics for monitoring. */
|
|
1340
|
+
async getMetrics() {
|
|
1341
|
+
const activeMembers = this.initialized
|
|
1342
|
+
? await this.db.listActiveMembers()
|
|
1343
|
+
: [];
|
|
1344
|
+
const currentEpochNumber = this.currentEpochData?.epochNumber ?? 0;
|
|
1345
|
+
return {
|
|
1346
|
+
proposals: {
|
|
1347
|
+
total: this.metricsData.proposalsTotal,
|
|
1348
|
+
pending: this.metricsData.proposalsPending,
|
|
1349
|
+
},
|
|
1350
|
+
votes: {
|
|
1351
|
+
latency_ms: this.metricsData.votesLatencyMs,
|
|
1352
|
+
},
|
|
1353
|
+
redistribution: {
|
|
1354
|
+
progress: this.metricsData.redistributionProgress,
|
|
1355
|
+
failures: this.metricsData.redistributionFailures,
|
|
1356
|
+
},
|
|
1357
|
+
members: {
|
|
1358
|
+
active: activeMembers.length,
|
|
1359
|
+
},
|
|
1360
|
+
epoch: {
|
|
1361
|
+
current: currentEpochNumber,
|
|
1362
|
+
},
|
|
1363
|
+
expiration: {
|
|
1364
|
+
last_run: this.metricsData.expirationLastRun,
|
|
1365
|
+
deleted_total: this.metricsData.expirationDeletedTotal,
|
|
1366
|
+
},
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/** Get the configured threshold for this quorum. */
|
|
1370
|
+
getConfiguredThreshold() {
|
|
1371
|
+
return this.configuredThreshold;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
exports.QuorumStateMachine = QuorumStateMachine;
|
|
1375
|
+
/**
|
|
1376
|
+
* Maximum allowed description length for proposals.
|
|
1377
|
+
*/
|
|
1378
|
+
QuorumStateMachine.MAX_DESCRIPTION_LENGTH = 4096;
|
|
1379
|
+
/**
|
|
1380
|
+
* Member count threshold above which inner quorum routing is used.
|
|
1381
|
+
*/
|
|
1382
|
+
QuorumStateMachine.INNER_QUORUM_MEMBER_THRESHOLD = 20;
|
|
1383
|
+
/**
|
|
1384
|
+
* Action types considered routine for inner quorum routing.
|
|
1385
|
+
* When member count > 20 and an inner quorum exists, these use the inner quorum threshold.
|
|
1386
|
+
*/
|
|
1387
|
+
QuorumStateMachine.ROUTINE_ACTION_TYPES = new Set([
|
|
1388
|
+
proposalActionType_1.ProposalActionType.ADD_MEMBER,
|
|
1389
|
+
proposalActionType_1.ProposalActionType.REMOVE_MEMBER,
|
|
1390
|
+
proposalActionType_1.ProposalActionType.UNSEAL_DOCUMENT,
|
|
1391
|
+
proposalActionType_1.ProposalActionType.REGISTER_ALIAS,
|
|
1392
|
+
proposalActionType_1.ProposalActionType.DEREGISTER_ALIAS,
|
|
1393
|
+
proposalActionType_1.ProposalActionType.EXTEND_STATUTE,
|
|
1394
|
+
proposalActionType_1.ProposalActionType.CUSTOM,
|
|
1395
|
+
]);
|
|
1396
|
+
//# sourceMappingURL=quorumStateMachine.js.map
|