Skip to content

Commit

Permalink
feat: solve race condition issue with VC status list index allocation
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Voiturier <[email protected]>
  • Loading branch information
bvoiturier committed Sep 30, 2024
1 parent 73a79f7 commit b35cbff
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 159 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
package org.hyperledger.identus.pollux.core.repository

import org.hyperledger.identus.pollux.core.model.*
import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021}
import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{
DecodingError,
EncodingError,
IndexOutOfBounds,
InvalidSize
}
import org.hyperledger.identus.pollux.vc.jwt.Issuer
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId}
import zio.*

import java.util.UUID

trait CredentialStatusListRepository {
def createStatusListVC(
jwtIssuer: Issuer,
statusListRegistryUrl: String,
id: UUID
): IO[Throwable, String] = {
for {
bitString <- BitString.getInstance().mapError {
case InvalidSize(message) => new Throwable(message)
case EncodingError(message) => new Throwable(message)
case DecodingError(message) => new Throwable(message)
case IndexOutOfBounds(message) => new Throwable(message)
}
emptyStatusListCredential <- VCStatusList2021
.build(
vcId = s"$statusListRegistryUrl/credential-status/$id",
revocationData = bitString,
jwtIssuer = jwtIssuer
)
.mapError(x => new Throwable(x.msg))

credentialWithEmbeddedProof <- emptyStatusListCredential.toJsonWithEmbeddedProof
} yield credentialWithEmbeddedProof.spaces2
}

def getCredentialStatusListIds: UIO[Seq[(WalletId, UUID)]]

def getCredentialStatusListsWithCreds(statusListId: UUID): URIO[WalletAccessContext, CredentialStatusListWithCreds]
Expand All @@ -16,17 +47,15 @@ trait CredentialStatusListRepository {
id: UUID
): UIO[Option[CredentialStatusList]]

def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]]
def incrementAndGetStatusListIndex(
jwtIssuer: Issuer,
statusListRegistryUrl: String
): URIO[WalletAccessContext, (UUID, Int)]

def existsForIssueCredentialRecordId(
id: DidCommID
): URIO[WalletAccessContext, Boolean]

def createNewForTheWallet(
jwtIssuer: Issuer,
statusListRegistryServiceName: String
): URIO[WalletAccessContext, CredentialStatusList]

def allocateSpaceForCredential(
issueCredentialRecordId: DidCommID,
credentialStatusListId: UUID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ object CredentialServiceImpl {
linkSecretService <- ZIO.service[LinkSecretService]
didService <- ZIO.service[DIDService]
manageDidService <- ZIO.service[ManagedDIDService]
issueCredentialSem <- Semaphore.make(1)
messageProducer <- ZIO.service[Producer[UUID, WalletIdAndRecordId]]
} yield CredentialServiceImpl(
credentialRepo,
Expand All @@ -72,7 +71,6 @@ object CredentialServiceImpl {
didService,
manageDidService,
5,
issueCredentialSem,
messageProducer
)
}
Expand All @@ -93,7 +91,6 @@ class CredentialServiceImpl(
didService: DIDService,
managedDIDService: ManagedDIDService,
maxRetries: Int = 5, // TODO move to config
issueCredentialSem: Semaphore,
messageProducer: Producer[UUID, WalletIdAndRecordId],
) extends CredentialService {

Expand Down Expand Up @@ -1311,32 +1308,26 @@ class CredentialServiceImpl(
record: IssueCredentialRecord,
statusListRegistryUrl: String,
jwtIssuer: JwtIssuer
): URIO[WalletAccessContext, CredentialStatus] = {
val effect = for {
lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet
currentStatusList <- lastStatusList
.fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))(
ZIO.succeed(_)
)
size = currentStatusList.size
lastUsedIndex = currentStatusList.lastUsedIndex
statusListToBeUsed <-
if lastUsedIndex < size then ZIO.succeed(currentStatusList)
else credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl)
): URIO[WalletAccessContext, CredentialStatus] =
for {
cslAndIndex <- credentialStatusListRepository.incrementAndGetStatusListIndex(
jwtIssuer,
statusListRegistryUrl
)
statusListId = cslAndIndex._1
indexInStatusList = cslAndIndex._2
_ <- credentialStatusListRepository.allocateSpaceForCredential(
issueCredentialRecordId = record.id,
credentialStatusListId = statusListToBeUsed.id,
statusListIndex = statusListToBeUsed.lastUsedIndex + 1
credentialStatusListId = statusListId,
statusListIndex = indexInStatusList
)
} yield CredentialStatus(
id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}",
id = s"$statusListRegistryUrl/credential-status/$statusListId#$indexInStatusList",
`type` = "StatusList2021Entry",
statusPurpose = StatusPurpose.Revocation,
statusListIndex = lastUsedIndex + 1,
statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}"
statusListIndex = indexInStatusList,
statusListCredential = s"$statusListRegistryUrl/credential-status/$statusListId"
)
issueCredentialSem.withPermit(effect)
}

override def generateAnonCredsCredential(
recordId: DidCommID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@ package org.hyperledger.identus.pollux.core.repository
import org.hyperledger.identus.castor.core.model.did.PrismDID
import org.hyperledger.identus.pollux.core.model.*
import org.hyperledger.identus.pollux.vc.jwt.{Issuer, StatusPurpose}
import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, VCStatusList2021}
import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.{
DecodingError,
EncodingError,
IndexOutOfBounds,
InvalidSize
}
import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId}
import zio.*

Expand Down Expand Up @@ -73,65 +67,69 @@ class CredentialStatusListRepositoryInMemory(
exists = stores.flatMap(_.values).exists(_.issueCredentialRecordId == id)
} yield exists

def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for {
storageRef <- walletToStatusListStorageRefs
storage <- storageRef.get
latest = storage.toSeq
.sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ }
.headOption
.map(_._2)
} yield latest

def createNewForTheWallet(
override def incrementAndGetStatusListIndex(
jwtIssuer: Issuer,
statusListRegistryUrl: String
): URIO[WalletAccessContext, CredentialStatusList] = {
): URIO[WalletAccessContext, (UUID, Int)] =
def getLatestOfTheWallet: URIO[WalletAccessContext, Option[CredentialStatusList]] = for {
storageRef <- walletToStatusListStorageRefs
storage <- storageRef.get
latest = storage.toSeq
.sortBy(_._2.createdAt) { (x, y) => if x.isAfter(y) then -1 else 1 /* DESC */ }
.headOption
.map(_._2)
} yield latest

val id = UUID.randomUUID()
val issued = Instant.now()
val issuerDid = jwtIssuer.did
val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical)
def createNewForTheWallet(
id: UUID,
jwtIssuer: Issuer,
issued: Instant,
credentialStr: String
): URIO[WalletAccessContext, CredentialStatusList] = {
val issuerDid = jwtIssuer.did
val canonical = PrismDID.fromString(issuerDid.toString).fold(e => throw RuntimeException(e), _.asCanonical)

val embeddedProofCredential = for {
bitString <- BitString.getInstance().mapError {
case InvalidSize(message) => new Throwable(message)
case EncodingError(message) => new Throwable(message)
case DecodingError(message) => new Throwable(message)
case IndexOutOfBounds(message) => new Throwable(message)
}
resourcePath =
s"credential-status/$id"
emptyJwtCredential <- VCStatusList2021
.build(
vcId = s"$statusListRegistryUrl/credential-status/$id",
revocationData = bitString,
jwtIssuer = jwtIssuer
for {
storageRef <- walletToStatusListStorageRefs
walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId)
newCredentialStatusList = CredentialStatusList(
id = id,
walletId = walletId,
issuer = canonical,
issued = issued,
purpose = StatusPurpose.Revocation,
statusListCredential = credentialStr,
size = BitString.MIN_SL2021_SIZE,
lastUsedIndex = 0,
createdAt = Instant.now(),
updatedAt = None
)
.mapError(x => new Throwable(x.msg))
_ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList))
} yield newCredentialStatusList
}

credentialWithEmbeddedProof <- emptyJwtCredential.toJsonWithEmbeddedProof
} yield credentialWithEmbeddedProof.spaces2
def updateLastUsedIndex(statusListId: UUID, lastUsedIndex: Int) =
for {
walletToStatusListStorageRef <- walletToStatusListStorageRefs
_ <- walletToStatusListStorageRef.update(r => {
val value = r.get(statusListId)
value.fold(r) { v =>
val updated = v.copy(lastUsedIndex = lastUsedIndex, updatedAt = Some(Instant.now))
r.updated(statusListId, updated)
}
})
} yield ()

for {
credential <- embeddedProofCredential.orDie
storageRef <- walletToStatusListStorageRefs
walletId <- ZIO.serviceWith[WalletAccessContext](_.walletId)
newCredentialStatusList = CredentialStatusList(
id = id,
walletId = walletId,
issuer = canonical,
issued = issued,
purpose = StatusPurpose.Revocation,
statusListCredential = credential,
size = BitString.MIN_SL2021_SIZE,
lastUsedIndex = 0,
createdAt = Instant.now(),
updatedAt = None
)
_ <- storageRef.update(r => r + (newCredentialStatusList.id -> newCredentialStatusList))
} yield newCredentialStatusList

}
id <- ZIO.succeed(UUID.randomUUID())
newStatusListVC <- createStatusListVC(jwtIssuer, statusListRegistryUrl, id).orDie
maybeStatusList <- getLatestOfTheWallet
statusList <- maybeStatusList match
case Some(csl) if csl.lastUsedIndex < csl.size => ZIO.succeed(csl)
case _ => createNewForTheWallet(id, jwtIssuer, Instant.now(), newStatusListVC)
newIndex = statusList.lastUsedIndex + 1
_ <- updateLastUsedIndex(statusList.id, newIndex)
} yield (statusList.id, newIndex)

def allocateSpaceForCredential(
issueCredentialRecordId: DidCommID,
Expand All @@ -152,14 +150,6 @@ class CredentialStatusListRepositoryInMemory(
for {
credentialInStatusListStorageRef <- statusListToCredInStatusListStorageRefs(credentialStatusListId)
_ <- credentialInStatusListStorageRef.update(r => r + (newCredentialInStatusList.id -> newCredentialInStatusList))
walletToStatusListStorageRef <- walletToStatusListStorageRefs
_ <- walletToStatusListStorageRef.update(r => {
val value = r.get(credentialStatusListId)
value.fold(r) { v =>
val updated = v.copy(lastUsedIndex = statusListIndex, updatedAt = Some(Instant.now))
r.updated(credentialStatusListId, updated)
}
})
} yield ()

}
Expand Down
Loading

0 comments on commit b35cbff

Please sign in to comment.