From 424e1036da3ea090d33780abb18350e8d5f9a871 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 07:42:26 +0000 Subject: [PATCH 01/14] store partial draft --- .../daml/Splice/Ans.daml | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index 43564af0c6..b91f10bd8a 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -283,6 +283,75 @@ template AnsEntryContext return AnsEntryContext_RejectEntryInitialPaymentResult with amuletSum = result.amuletSum +template AnsConfigV2 with + dso : Party + entryTTL : RelTime + where + signatory dso + +{- + +canton.network/url# : url + + +-} + +template AnsEntryV2 + with + dso : Party + publisher : Party + holder : Party + validFrom : Optional Time + validUntil : Optional Time + claims : TextMap Text + createdAt : Time + meta : Metadata + where + ensure validEntry this + + signatory publisher, holder + observer dso + + choice AnsEntryV2_DsoInvalidCreatedAt : () + controller dso + do + -- created at cannot be in the future + assertWithinDeadline "createdAt" createdAt + pure () + + choice AnsEntryV2_Update : ContractId AnsEntryV2 + with + newCreatedAt : Time + actor : Party + controller actor + do + assertDeadlineExceeded "newCreatedAt" newCreatedAt + create this with + createdAt = newCreatedAt + + choice AnsEntryV2_DsoExpire : () + with + configCid : ContractId AnsConfigV2 + controller dso + do + config <- fetchChecked (ForDso dso) configCid + assertDeadlineExceeded "createdAt + ansConfig.entryTTL" (createdAt `addRelTime` config.entryTTL) + pure () + + +validEntry : AnsEntryV2 -> Bool +validEntry AnsEntryV2{..} = + TextMap.size claims <= 10 && + -- only printable ASCII characters in claim keys + -- at most 400 characters per claim key + -- at most 2000 characters per claim value + case (validFrom, validUntil) of + (Some from, Some until) -> createdAt <= from && from < until + (Some from, None) -> createdAt <= from + (None, Some until) -> createdAt < until + (None, None) -> True + + -- | A ans entry that needs to be renewed continuously. -- Renewal recreates this contract with an updated `expiresAt` field. @@ -294,8 +363,23 @@ template AnsEntry url : Text -- either empty or contains a valid http/https url description : Text -- can be empty expiresAt : Time + + where signatory user, dso +{- + + issuer: dso + holder: user + validFrom: None + validUntil: Some(expiresAt) + claims: + cns.canton.network/name: name + cns.canton.network//url: url + cns.canton.network//description: description + +-} + choice AnsEntry_Expire : AnsEntry_ExpireResult with From 58990f1b75ddf04e2ff9b5763c8f73b41a4ddc81 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 08:14:45 +0000 Subject: [PATCH 02/14] before renaming --- .../daml/Splice/AnsV2/AnsCredential.daml | 99 +++++++++++++++++++ .../daml/Splice/AnsV2/CredentialRegistry.daml | 78 +++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml create mode 100644 daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml new file mode 100644 index 0000000000..f42e2f2276 --- /dev/null +++ b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml @@ -0,0 +1,99 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.AnsV2.AnsCredential where + +import DA.Action (void) +import DA.Time + +import Splice.Amulet +import Splice.AmuletRules +import Splice.Types +import Splice.Wallet.Payment +import Splice.Wallet.Subscriptions +import Splice.Util + +{- + +canton.network/url# : url + + +-} + +data Credential = Credential with + claims : TextMap Text + validFrom : Optional Time + validUntil : Optional Time + createdAt : Time + meta : Metadata + deriving (Eq, Show) + +interface CredentialRegistry where + -- | Register a new credential with the DSO. + registerCredential : Party -> Credential -> Update (ContractId AnsCredential) + + + nonconsuming choice CredentialRegistry_UpdateCredentials + : ContractId Credential + with + inputCredentials : [ContractId Credential] + newCredentials : [Credential] + actors : Party + expectedRegisty : Party + controller actors + do updateCredentials + + + + +-- | A credential visible to the DSO and served by its Scan service. +template AnsCredential + with + dso : Party + issuer : Party + holder : Party + credential : Credential + where + ensure validEntry this + + signatory issuer, holder + observer dso + + choice AnsCredential_DsoInvalidCreatedAt : () + controller dso + do + -- created at cannot be in the future + assertWithinDeadline "createdAt" createdAt + pure () + + choice AnsCredential_Update : ContractId AnsCredential + with + newCreatedAt : Time + actor : Party + controller actor + do + assertDeadlineExceeded "newCreatedAt" newCreatedAt + create this with + createdAt = newCreatedAt + + choice AnsCredential_DsoExpire : () + with + configCid : ContractId AnsConfigV2 + controller dso + do + config <- fetchChecked (ForDso dso) configCid + assertDeadlineExceeded "createdAt + ansConfig.entryTTL" (createdAt `addRelTime` config.entryTTL) + pure () + + +validEntry : AnsCredential -> Bool +validEntry AnsCredential{..} = + TextMap.size claims <= 10 && + -- only printable ASCII characters in claim keys + -- at most 400 characters per claim key + -- at most 2000 characters per claim value + case (validFrom, validUntil) of + (Some from, Some until) -> createdAt <= from && from < until + (Some from, None) -> createdAt <= from + (None, Some until) -> createdAt < until + (None, None) -> True diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml new file mode 100644 index 0000000000..49ad8ba97b --- /dev/null +++ b/daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml @@ -0,0 +1,78 @@ + +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Api.Credential.Registry where + +import DA.Action (void) +import DA.Time + +import Splice.Amulet +import Splice.AmuletRules +import Splice.Types +import Splice.Wallet.Payment +import Splice.Wallet.Subscriptions +import Splice.Util + +import Splice.Api.Token.Metadata + +{- + +canton.network/url# : url + + +-} + +data CredentialSpecification = CredentialSpecification with + claims : TextMap Text + validFrom : Optional Time + validUntil : Optional Time + createdAt : Time + meta : Metadata + deriving (Eq, Show) + +data CredentialView = CredentialView with + admin : Party + issuer : Party + holder : Party + credential : CredentialSpecification + expiresAt : Optional Time + deriving (Eq, Show) + +interface Credential where + viewtype CredentialView + + credential_publicFetchImpl : ContractId Credential -> Credential_PublicFetch -> Update CredentialView + + choice Credential_PublicFetch : CredentialView + actor : Party + -- ^ The party fetching the credential. + controller actor + do credential_publicFetchImpl this self arg + + +data CredentialRegistryView = CredentialRegistryView with + admin : Party + meta : Metadata + deriving (Eq, Show) + +interface CredentialRegistry where + viewtype CredentialRegistryView + + credentialRegistry_updateCredentialsImpl : ContractId CredentialRegistry -> CredentialRegistry_UpdateCredentials -> Update (ContractId Credential) + + + nonconsuming choice CredentialRegistry_UpdateCredentials + : CredentialRegistry_UpdateCredentialsResult + with + oldCredentials : [ContractId Credential] + newCredentials : [Credential] + actors : Party + expectedAdmin : Party + controller actors + do credentialRegistry_updateCredentialsImpl this self arg + +data CredentialRegistry_UpdateCredentialsResult = CredentialRegistry_UpdateCredentialsResult with + newCredentialCids : [ContractId Credential] + meta : Metadata + deriving (Eq, Show) From abfbcaef50a50d79a53e47e51d89856368e8e3af Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 09:03:41 +0000 Subject: [PATCH 03/14] registry interface specced --- .../daml/Splice/AnsV2/CredentialRegistry.daml | 78 ------------ .../Splice/Api/Credential/RegistryV1.daml | 115 ++++++++++++++++++ 2 files changed, 115 insertions(+), 78 deletions(-) delete mode 100644 daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml create mode 100644 daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml deleted file mode 100644 index 49ad8ba97b..0000000000 --- a/daml/splice-amulet-name-service/daml/Splice/AnsV2/CredentialRegistry.daml +++ /dev/null @@ -1,78 +0,0 @@ - --- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. --- SPDX-License-Identifier: Apache-2.0 - -module Splice.Api.Credential.Registry where - -import DA.Action (void) -import DA.Time - -import Splice.Amulet -import Splice.AmuletRules -import Splice.Types -import Splice.Wallet.Payment -import Splice.Wallet.Subscriptions -import Splice.Util - -import Splice.Api.Token.Metadata - -{- - -canton.network/url# : url - - --} - -data CredentialSpecification = CredentialSpecification with - claims : TextMap Text - validFrom : Optional Time - validUntil : Optional Time - createdAt : Time - meta : Metadata - deriving (Eq, Show) - -data CredentialView = CredentialView with - admin : Party - issuer : Party - holder : Party - credential : CredentialSpecification - expiresAt : Optional Time - deriving (Eq, Show) - -interface Credential where - viewtype CredentialView - - credential_publicFetchImpl : ContractId Credential -> Credential_PublicFetch -> Update CredentialView - - choice Credential_PublicFetch : CredentialView - actor : Party - -- ^ The party fetching the credential. - controller actor - do credential_publicFetchImpl this self arg - - -data CredentialRegistryView = CredentialRegistryView with - admin : Party - meta : Metadata - deriving (Eq, Show) - -interface CredentialRegistry where - viewtype CredentialRegistryView - - credentialRegistry_updateCredentialsImpl : ContractId CredentialRegistry -> CredentialRegistry_UpdateCredentials -> Update (ContractId Credential) - - - nonconsuming choice CredentialRegistry_UpdateCredentials - : CredentialRegistry_UpdateCredentialsResult - with - oldCredentials : [ContractId Credential] - newCredentials : [Credential] - actors : Party - expectedAdmin : Party - controller actors - do credentialRegistry_updateCredentialsImpl this self arg - -data CredentialRegistry_UpdateCredentialsResult = CredentialRegistry_UpdateCredentialsResult with - newCredentialCids : [ContractId Credential] - meta : Metadata - deriving (Eq, Show) diff --git a/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml new file mode 100644 index 0000000000..80d73210e9 --- /dev/null +++ b/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml @@ -0,0 +1,115 @@ + +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | Credential Registry API. +module Splice.Api.Credential.RegistryV1 where + +import DA.Time + +import Splice.Api.Token.MetadataV1 + + +-- | A credential modelled after the W3C Verifiable Credential data model. +data Credential = Credential with + claims : TextMap Text + -- ^ Claims are encoded as key-value pairs to align with the common use of + -- key-value pairs in ENS, DNS, k8s, and similar systems. A W3C claim + -- of the form (subject, property, value) is represented as a key-value pair + -- with the key formed as "property#subject" and value as "value". + -- + -- When no '#subject' suffix is present in the key, the claim pertains + -- to the holder of the credential. Thus the key value pair + -- + -- "profile.displayName" : "Alice" + -- + -- corresponds to the W3C claim (subject=holder, property="profile.displayName", value="Alice"). + -- + -- Implementations SHOULD ensure that claims are stored in canonical form without + -- redundant '#holder' suffixes in keys. + -- + -- Keys SHOULD be namespaced in the form `dns.name/key` to avoid + -- collisions between different usecases. + + validFrom : Optional Time + -- ^ The time from which this credential is valid. + validUntil : Optional Time + -- ^ The time until which this credential is valid. + meta : Metadata + -- ^ Metadata associated with this credential. Used for extensibility. + deriving (Eq, Show) + +-- | A view of a credential record stored in a credential registry. +data CredentialRecordView = CredentialRecordView with + admin : Party + -- ^ The party that administers this credential registry. + issuer : Party + -- ^ The party that issued the credential. + holder : Party + -- ^ The party that holds the credential. + credential : Credential + -- ^ The credential associated with this record. + createdAt : Optional Time + -- ^ The time at which this credential record was created. + expiresAt : Optional Time + -- ^ The time at which this credential record expires. + -- The registry MAY archive the record after this time. + meta : Metadata + -- ^ Metadata associated with this credential record. Used for extensibility. + deriving (Eq, Show) + +-- | A credential record stored in a credential registry. +interface CredentialRecord where + viewtype CredentialRecordView + + +-- Registry interface +--------------------- + +-- | Credential registry interface. +interface CredentialRegistry where + viewtype CredentialRegistryView + + credentialRegistry_updateRecordsImpl : ContractId CredentialRegistry -> CredentialRegistry_UpdateRecords -> Update CredentialRegistry_UpdateRecordsResult + + nonconsuming choice CredentialRegistry_UpdateRecords : CredentialRegistry_UpdateRecordsResult + -- ^ Update a set of credential records in the registry. + with + admin : Party -- ^ The admin of the credential records being updated. + holder : Party -- ^ The holder of the credential records being updated. + issuer : Party -- ^ The issuer of the credential records being updated. + oldRecords : [ContractId CredentialRecord] + -- ^ The existing credential records to archive. + -- + -- MAY be empty if no prior record exists. + newCredentials : [Credential] + -- ^ The new credentials to record. + newCreatedAt : Time + -- ^ The creation time to use for the new credential records. + -- + -- Implementations MAY use this as the basis for computing the expiry time of the record. + -- Callers MUST ensure that `newCreatedAt` is in the past and they likely want it to be close to the current time + -- to maximize the lifetime of the credential record. + -- + -- It is a required argument so that implementations can compute the expiry time of the record + -- without depending on `getTime`, as using `getTime` limits the delay between + -- preparing and submitting the transaction to a 1 minute window. + + extraArgs : ExtraArgs + -- ^ The extra arguments to pass to the implementation. + controller holder, issuer + do credentialRegistry_updateRecordsImpl this self arg + + +-- | A view of a credential registry. +data CredentialRegistryView = CredentialRegistryView with + admin : Party -- ^ The party that administers this credential registry. + meta : Metadata -- ^ Metadata associated with this credential registry. Used for extensibility. + deriving (Eq, Show) + +data CredentialRegistry_UpdateRecordsResult = CredentialRegistry_UpdateRecordsResult with + newRecords : [ContractId CredentialRecord] + -- ^ The newly created credential records. + meta : Metadata + -- ^ Additional metadata specific to the update operation, used for extensibility. + deriving (Eq, Show) From bfbc70f6cd19da7188e2f2ac1adfc239bfd8ed13 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 09:38:18 +0000 Subject: [PATCH 04/14] strawan registry implementation --- daml/splice-amulet-name-service/daml.yaml | 2 + .../daml/Splice/Ans.daml | 89 +++-------- .../daml/Splice/AnsV2/AnsCredential.daml | 150 +++++++++++++----- .../Splice/Api/Credential/RegistryV1.daml | 2 +- 4 files changed, 134 insertions(+), 109 deletions(-) diff --git a/daml/splice-amulet-name-service/daml.yaml b/daml/splice-amulet-name-service/daml.yaml index 83452c24b4..ead551406c 100644 --- a/daml/splice-amulet-name-service/daml.yaml +++ b/daml/splice-amulet-name-service/daml.yaml @@ -10,10 +10,12 @@ data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar - ../splice-api-featured-app-v1/.daml/dist/splice-api-featured-app-v1-current.dar +- ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar build-options: - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - -Wno-ledger-time-is-alpha + - -Wupgrade-interfaces # FIXME: remove once the interface files moved - --target=2.1 codegen: java: diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index b91f10bd8a..533639a19f 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -4,6 +4,7 @@ module Splice.Ans where import DA.Action (void) +import DA.TextMap qualified as TM import DA.Time import Splice.Amulet @@ -13,6 +14,9 @@ import Splice.Wallet.Payment import Splice.Wallet.Subscriptions import Splice.Util +import Splice.Api.Token.MetadataV1 (emptyMetadata) +import Splice.Api.Credential.RegistryV1 qualified as RegistryV1 + data AnsRules_RequestEntryResult = AnsRules_RequestEntryResult with entryCid : ContractId AnsEntryContext @@ -283,73 +287,7 @@ template AnsEntryContext return AnsEntryContext_RejectEntryInitialPaymentResult with amuletSum = result.amuletSum -template AnsConfigV2 with - dso : Party - entryTTL : RelTime - where - signatory dso - -{- - -canton.network/url# : url - - --} - -template AnsEntryV2 - with - dso : Party - publisher : Party - holder : Party - validFrom : Optional Time - validUntil : Optional Time - claims : TextMap Text - createdAt : Time - meta : Metadata - where - ensure validEntry this - - signatory publisher, holder - observer dso - choice AnsEntryV2_DsoInvalidCreatedAt : () - controller dso - do - -- created at cannot be in the future - assertWithinDeadline "createdAt" createdAt - pure () - - choice AnsEntryV2_Update : ContractId AnsEntryV2 - with - newCreatedAt : Time - actor : Party - controller actor - do - assertDeadlineExceeded "newCreatedAt" newCreatedAt - create this with - createdAt = newCreatedAt - - choice AnsEntryV2_DsoExpire : () - with - configCid : ContractId AnsConfigV2 - controller dso - do - config <- fetchChecked (ForDso dso) configCid - assertDeadlineExceeded "createdAt + ansConfig.entryTTL" (createdAt `addRelTime` config.entryTTL) - pure () - - -validEntry : AnsEntryV2 -> Bool -validEntry AnsEntryV2{..} = - TextMap.size claims <= 10 && - -- only printable ASCII characters in claim keys - -- at most 400 characters per claim key - -- at most 2000 characters per claim value - case (validFrom, validUntil) of - (Some from, Some until) -> createdAt <= from && from < until - (Some from, None) -> createdAt <= from - (None, Some until) -> createdAt < until - (None, None) -> True @@ -363,8 +301,6 @@ template AnsEntry url : Text -- either empty or contains a valid http/https url description : Text -- can be empty expiresAt : Time - - where signatory user, dso {- @@ -380,6 +316,23 @@ template AnsEntry -} + interface instance RegistryV1.CredentialRecord for AnsEntry where + view = RegistryV1.CredentialRecordView with + admin = dso + issuer = dso + holder = user + credential = RegistryV1.Credential with + claims = TM.fromList [ + ("ans.name", name), + ("ans.url#" <> name, url), + ("ans.description#" <> name, description) + ] + validFrom = None + validUntil = Some expiresAt + meta = emptyMetadata + createdAt = None + expiresAt = Some expiresAt + meta = emptyMetadata choice AnsEntry_Expire : AnsEntry_ExpireResult with diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml index f42e2f2276..8cd3d8b195 100644 --- a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml +++ b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml @@ -1,64 +1,94 @@ -- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 +-- | Amulet Name Service (ANS) V2 Credential registry implementation. module Splice.AnsV2.AnsCredential where -import DA.Action (void) +import DA.Assert +import DA.Foldable (forA_) import DA.Time -import Splice.Amulet -import Splice.AmuletRules import Splice.Types -import Splice.Wallet.Payment -import Splice.Wallet.Subscriptions import Splice.Util -{- +import Splice.Api.Token.MetadataV1 (emptyMetadata) +import Splice.Api.Credential.RegistryV1 qualified as RegistryV1 -canton.network/url# : url +template AnsCredentialRegistry with + dso : Party + where + signatory dso + interface instance RegistryV1.CredentialRegistry for AnsCredentialRegistry where + view = RegistryV1.CredentialRegistryView with + admin = dso + meta = emptyMetadata --} + credentialRegistry_updateRecordsImpl self RegistryV1.CredentialRegistry_UpdateRecords{..} = do + assertDeadlineExceeded "newCreatedAt" newCreatedAt + -- FIXME: make configurable + let defaultExpiresAt = addRelTime newCreatedAt (days 90) -- default to 90 day expiry -data Credential = Credential with - claims : TextMap Text - validFrom : Optional Time - validUntil : Optional Time - createdAt : Time - meta : Metadata - deriving (Eq, Show) + -- check admin + require "admin matches" (admin == dso) + -- archive old records + forA_ oldRecords \oldRecordCid -> do + let ansRecordCid = fromInterfaceContractId @AnsCredentialRecord oldRecordCid + oldRecord <- fetchAndArchive (ForOwner with dso; owner = holder) ansRecordCid + require "issuer matches" (oldRecord.issuer == issuer) -interface CredentialRegistry where - -- | Register a new credential with the DSO. - registerCredential : Party -> Credential -> Update (ContractId AnsCredential) - - - nonconsuming choice CredentialRegistry_UpdateCredentials - : ContractId Credential - with - inputCredentials : [ContractId Credential] - newCredentials : [Credential] - actors : Party - expectedRegisty : Party - controller actors - do updateCredentials + -- create new records + newRecords <- forA newCredentials \newCredential -> do + toInterfaceContractId <$> create AnsCredentialRecord with + dso + issuer + holder + credential = newCredential + createdAt = newCreatedAt + expiresAt = optional defaultExpiresAt (min defaultExpiresAt) newCredential.validUntil + pure RegistryV1.CredentialRegistry_UpdateRecordsResult with + newRecords + meta = emptyMetadata -- | A credential visible to the DSO and served by its Scan service. -template AnsCredential +template AnsCredentialRecord with dso : Party issuer : Party holder : Party - credential : Credential + credential : RegistryV1.Credential + createdAt : Time + expiresAt : Time where - ensure validEntry this + -- ensure validEntry this signatory issuer, holder observer dso + interface instance RegistryV1.CredentialRecord for AnsCredentialRecord where + view = RegistryV1.CredentialRecordView with + admin = dso + issuer = issuer + holder = holder + credential = credential + createdAt = Some createdAt + expiresAt = Some expiresAt + meta = emptyMetadata + + choice AnsCredential_Expire : () + with + actor : Party + controller actor + do + require "actor is stakeholder" (actor == holder || actor == issuer || actor == dso) + assertDeadlineExceeded "expiresAt" expiresAt + + -- TODO: add choice for Dso to remove invalid credential records + +{- choice AnsCredential_DsoInvalidCreatedAt : () controller dso do @@ -76,14 +106,49 @@ template AnsCredential create this with createdAt = newCreatedAt - choice AnsCredential_DsoExpire : () - with - configCid : ContractId AnsConfigV2 - controller dso - do - config <- fetchChecked (ForDso dso) configCid - assertDeadlineExceeded "createdAt + ansConfig.entryTTL" (createdAt `addRelTime` config.entryTTL) - pure () +-} + +{- + +{- + +template AnsConfigV2 with + dso : Party + entryTTL : RelTime + where + signatory dso + +canton.network/url# : url + + +-} + +data Credential = Credential with + claims : TextMap Text + validFrom : Optional Time + validUntil : Optional Time + createdAt : Time + meta : Metadata + deriving (Eq, Show) + +interface CredentialRegistry where + -- | Register a new credential with the DSO. + registerCredential : Party -> Credential -> Update (ContractId AnsCredential) + + + nonconsuming choice CredentialRegistry_UpdateCredentials + : ContractId Credential + with + inputCredentials : [ContractId Credential] + newCredentials : [Credential] + actors : Party + expectedRegisty : Party + controller actors + do updateCredentials + + + + validEntry : AnsCredential -> Bool @@ -97,3 +162,8 @@ validEntry AnsCredential{..} = (Some from, None) -> createdAt <= from (None, Some until) -> createdAt < until (None, None) -> True + +-} + +instance HasCheckedFetch AnsCredentialRecord ForOwner where + contractGroupId AnsCredentialRecord{..} = ForOwner with dso; owner = holder diff --git a/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml index 80d73210e9..491f584fe4 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml @@ -5,7 +5,7 @@ -- | Credential Registry API. module Splice.Api.Credential.RegistryV1 where -import DA.Time +import DA.TextMap (TextMap) import Splice.Api.Token.MetadataV1 From 54dc01a7ac6ad2a1d84c120ea73974e3b8507bda Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 09:59:51 +0000 Subject: [PATCH 05/14] create separate library for interfaces --- build.sbt | 27 ++++++-- daml/splice-amulet-name-service/daml.yaml | 3 +- .../daml/Splice/AnsV2/AnsCredential.daml | 67 +++---------------- .../daml.yaml | 18 +++++ .../Splice/Api/Credential/RegistryV1.daml | 0 5 files changed, 52 insertions(+), 63 deletions(-) create mode 100644 daml/splice-api-credential-registry-v1/daml.yaml rename daml/{splice-amulet-name-service => splice-api-credential-registry-v1}/daml/Splice/Api/Credential/RegistryV1.daml (100%) diff --git a/build.sbt b/build.sbt index 9ba810a7b8..76a8044645 100644 --- a/build.sbt +++ b/build.sbt @@ -114,6 +114,8 @@ lazy val root: Project = (project in file(".")) `splice-dso-governance-test-daml`, `splice-validator-lifecycle-daml`, `splice-validator-lifecycle-test-daml`, + `splice-api-featured-app-v1-daml`, + `splice-api-credential-registry-v1-daml`, `splice-api-token-metadata-v1-daml`, `splice-api-token-holding-v1-daml`, `splice-api-token-transfer-instruction-v1-daml`, @@ -660,7 +662,7 @@ lazy val `splice-util-daml` = `canton-bindings-java` ) -lazy val `splice-featured-app-api-v1-daml` = +lazy val `splice-api-featured-app-v1-daml` = project .in(file("daml/splice-api-featured-app-v1")) .enablePlugins(DamlPlugin) @@ -671,6 +673,21 @@ lazy val `splice-featured-app-api-v1-daml` = `canton-bindings-java` ) +lazy val `splice-api-credential-registry-v1-daml` = + project + .in(file("daml/splice-api-credential-registry-v1")) + .enablePlugins(DamlPlugin) + .settings( + BuildCommon.damlSettings, + Compile / damlDependencies := + (`splice-util-daml` / Compile / damlBuild).value ++ + (`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value, + ) + .dependsOn( + `canton-bindings-java` + ) + + lazy val `splice-amulet-daml` = project .in(file("daml/splice-amulet")) @@ -685,7 +702,7 @@ lazy val `splice-amulet-daml` = (`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-request-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value, ) .dependsOn(`canton-bindings-java`) @@ -796,7 +813,7 @@ lazy val `splice-util-featured-app-proxies-daml` = (`splice-api-token-transfer-instruction-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value, ) .dependsOn(`canton-bindings-java`) @@ -810,7 +827,7 @@ lazy val `splice-util-token-standard-wallet-daml` = (`splice-api-token-holding-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-transfer-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value, ) .dependsOn(`canton-bindings-java`) @@ -922,7 +939,7 @@ lazy val `apps-common` = `splice-api-token-allocation-instruction-v1-daml`, `splice-token-test-dummy-holding-daml`, `splice-token-test-trading-app-daml`, - `splice-featured-app-api-v1-daml`, + `splice-api-featured-app-v1-daml`, ) .enablePlugins(BuildInfoPlugin) .settings( diff --git a/daml/splice-amulet-name-service/daml.yaml b/daml/splice-amulet-name-service/daml.yaml index ead551406c..76462f285c 100644 --- a/daml/splice-amulet-name-service/daml.yaml +++ b/daml/splice-amulet-name-service/daml.yaml @@ -9,13 +9,14 @@ data-dependencies: - ../splice-amulet/.daml/dist/splice-amulet-current.dar - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar + - ../splice-api-featured-app-v1/.daml/dist/splice-api-featured-app-v1-current.dar +- ../splice-api-credential-registry-v1/.daml/dist/splice-api-credential-registry-v1-current.dar - ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar build-options: - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - -Wno-ledger-time-is-alpha - - -Wupgrade-interfaces # FIXME: remove once the interface files moved - --target=2.1 codegen: java: diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml index 8cd3d8b195..158dd36113 100644 --- a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml +++ b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml @@ -14,6 +14,8 @@ import Splice.Util import Splice.Api.Token.MetadataV1 (emptyMetadata) import Splice.Api.Credential.RegistryV1 qualified as RegistryV1 + +-- | Registry for creating ANS records. template AnsCredentialRegistry with dso : Party where @@ -24,9 +26,9 @@ template AnsCredentialRegistry with admin = dso meta = emptyMetadata - credentialRegistry_updateRecordsImpl self RegistryV1.CredentialRegistry_UpdateRecords{..} = do + credentialRegistry_updateRecordsImpl _self RegistryV1.CredentialRegistry_UpdateRecords{..} = do assertDeadlineExceeded "newCreatedAt" newCreatedAt - -- FIXME: make configurable + -- TODO: make configurable let defaultExpiresAt = addRelTime newCreatedAt (days 90) -- default to 90 day expiry -- check admin @@ -86,69 +88,20 @@ template AnsCredentialRecord require "actor is stakeholder" (actor == holder || actor == issuer || actor == dso) assertDeadlineExceeded "expiresAt" expiresAt - -- TODO: add choice for Dso to remove invalid credential records - -{- - choice AnsCredential_DsoInvalidCreatedAt : () + choice AnsCredential_DsoArchiveInvalid : () + -- ^ Archive this credential record if it is invalid according to the DSO rules. + -- Required as the DSO party is only an observer and thus cannot control the creation of records. controller dso do -- created at cannot be in the future assertWithinDeadline "createdAt" createdAt + -- TODO: add check that expiresAt is not too far in the future + -- TODO: consider how to check that reliably when the allowed TTL changes pure () - choice AnsCredential_Update : ContractId AnsCredential - with - newCreatedAt : Time - actor : Party - controller actor - do - assertDeadlineExceeded "newCreatedAt" newCreatedAt - create this with - createdAt = newCreatedAt - --} - -{- - -{- - -template AnsConfigV2 with - dso : Party - entryTTL : RelTime - where - signatory dso - -canton.network/url# : url - - --} - -data Credential = Credential with - claims : TextMap Text - validFrom : Optional Time - validUntil : Optional Time - createdAt : Time - meta : Metadata - deriving (Eq, Show) - -interface CredentialRegistry where - -- | Register a new credential with the DSO. - registerCredential : Party -> Credential -> Update (ContractId AnsCredential) - - - nonconsuming choice CredentialRegistry_UpdateCredentials - : ContractId Credential - with - inputCredentials : [ContractId Credential] - newCredentials : [Credential] - actors : Party - expectedRegisty : Party - controller actors - do updateCredentials - - +{- TODO: consider exact validation rules and implement them validEntry : AnsCredential -> Bool diff --git a/daml/splice-api-credential-registry-v1/daml.yaml b/daml/splice-api-credential-registry-v1/daml.yaml new file mode 100644 index 0000000000..ea7b4aadab --- /dev/null +++ b/daml/splice-api-credential-registry-v1/daml.yaml @@ -0,0 +1,18 @@ +sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 +name: splice-api-credential-registry-v1 +source: daml +version: 1.0.0 +dependencies: + - daml-prim + - daml-stdlib +data-dependencies: + - ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar +build-options: + - --ghc-option=-Wunused-binds + - --ghc-option=-Wunused-matches + - --target=2.1 +codegen: + java: + package-prefix: org.lfdecentralizedtrust.splice.codegen.java + decoderClass: org.lfdecentralizedtrust.splice.codegen.java.DecoderSpliceCredentialRegistryV1 + output-directory: target/daml-codegen-java diff --git a/daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml similarity index 100% rename from daml/splice-amulet-name-service/daml/Splice/Api/Credential/RegistryV1.daml rename to daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml From e276b4ef4c1318108e50955f004a067dbf1bc283 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 11:36:00 +0000 Subject: [PATCH 06/14] add sketch of OpenAPI spec --- .../Splice/Api/Credential/RegistryV1.daml | 4 +- .../openapi/README.md | 30 +++ .../openapi/credential-registry-v1.yaml | 228 ++++++++++++++++++ .../openapi/docker-compose.yml | 16 ++ 4 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 daml/splice-api-credential-registry-v1/openapi/README.md create mode 100644 daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml create mode 100644 daml/splice-api-credential-registry-v1/openapi/docker-compose.yml diff --git a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml index 491f584fe4..c847617e91 100644 --- a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml @@ -28,9 +28,11 @@ data Credential = Credential with -- Implementations SHOULD ensure that claims are stored in canonical form without -- redundant '#holder' suffixes in keys. -- - -- Keys SHOULD be namespaced in the form `dns.name/key` to avoid + -- Keys MUST only contain characters from [a-zA-Z0-9._:-] + -- and SHOULD be namespaced in the form `dns.name/key` to avoid -- collisions between different usecases. + validFrom : Optional Time -- ^ The time from which this credential is valid. validUntil : Optional Time diff --git a/daml/splice-api-credential-registry-v1/openapi/README.md b/daml/splice-api-credential-registry-v1/openapi/README.md new file mode 100644 index 0000000000..7f7b225cbc --- /dev/null +++ b/daml/splice-api-credential-registry-v1/openapi/README.md @@ -0,0 +1,30 @@ + +### Implementation note + +We expect Scan to maintain the following indices. +We expect Scan to use them in the following priority order: + +``` +-- Used when holder is specified. +create index idx_credentialrecord_holder ON CredentialRecord(holder, property, issuer, record_time); + +-- Used when a property prefix is specified. +create index idx_credentialrecord_issuer ON CredentialRecord(property, issuer, record_time); + +-- Used when issuer is specified. +create index idx_credentialrecord_issuer ON CredentialRecord(issuer, record_time); + +-- Used when no filters are specified. +create index idx_credentialrecord_recordtime ON CredentialRecord(record_time); +``` + +The queries are constructed such that pagination follows the index order. +They use a deterministic paging order based on the index columns, where `record_time` and +`contract_id` are used as tie breakers. +An ascending order for record_time is chosen to implement first write wins +semantics, which is preferred as it incentivizes archiving older records. + +The fallback order can be used to page through all records in the order in which +they were created; and it can also be used to tail the table to ingest all records as +they are created. + diff --git a/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml b/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml new file mode 100644 index 0000000000..3196a5f581 --- /dev/null +++ b/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml @@ -0,0 +1,228 @@ +# Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +openapi: 3.0.0 +info: + title: credential registry off-ledger API + description: | + Implemented by credential registries for the purpose of creating, updating, + and querying credential records. + version: 1.0.0 +paths: + + # TODO: fix the schema validation errors. The current state is meant as a sketch to align on the design. + + # TODO: add info endpoint where one can learn about limits of the registry etc. + + # TODO: consider renaming "CredentialRegistry" to "CredentialFactory" to be consistent with the token standard APIs; and to make the call below less confusing + /registry/credentials/v1/credential-registry + post: + operationId: "getCredentialRegistry" + description: | + Get the credential registry contract and the choice context for updating records. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GetCredentialRegistryRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/CredentialsRegistryWithChoiceContext" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + + + /registry/credentials/v1/credentials + get: + operationId: "listCredentials" + description: | + List the credential records matching the provided filters. + + Note that registries MAY decide to limit the number of records returned. + + Decentralized registries MAY also store a record_time cutoff in the page token to make the + responses more stable from a pagination perspective. + parameters: + - in: query + name: holder + schema: + type: string + required: false + description: | + The party that holds the credential records. + - in: query + name: issuer + schema: + type: string + required: false + description: | + The party that issued the credential records. + - in: query + name: keyPrefix + schema: + type: string + required: false + description: | + The prefix of the keys of the credential records. + - in: query + name: limit + schema: + type: integer + format: int32 + required: false + description: | + The maximum number of credential records to return. + - in: query + name: pageToken + schema: + type: string + required: false + description: | + The token for the page to retrieve. + - in: query + name: validAsOf + schema: + type: string + format: date-time + required: false + description: | + If provided, only return credential records that are valid at the given time. + + TODO: consider whether we can use a `validAsOf` date that is + slightly in the past to implement stable pagination for + decentralized registries. + + - in: query + name: includeDisclosedContracts + schema: + type: boolean + default: false + required: false + description: | + If set to true, the response will include the disclosed contracts of the credentials + for use with explicit contract disclosure upon command submission. + - in: query + name: excludeDebugFields + schema: + type: boolean + default: false + required: false + description: | + If set to true, then the disclosed contracts will not include debug fields. + + responses: + "200": + description: ok + content: + application/json: + schema: + type: object + properties: + credentials: + type: array + items: + $ref: "#/components/schemas/CredentialRecord" + description: | + The list of credential records matching the provided filters. + nextPageToken: + type: string + description: | + The token for the next page of results. Omitted if there are no more results. + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + + + +components: + responses: + "400": + description: "bad request" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "404": + description: "not found" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + schemas: + GetCredentialRegistryRequest: + # TODO: fill out + + CredentialRecord: + type: object + properties: + credentialRecordView: + type: object + description: | + The view computed for the credential record with the schema of Splice.Api.Credential.RegistryV1.CredentialRecordView. + disclosedContract: + $ref: "#/components/schemas/DisclosedContract" + description: | + The disclosed contract information for verifying the credential record using explicit contract disclosure. + required: + [ + "credentialRecordView", + ] + + # Note: intentionally not shared with the other APIs to keep the self-contained, and because not all OpenAPI codegens support such shared definitions. + DisclosedContract: + type: object + properties: + templateId: + type: string + contractId: + type: string + createdEventBlob: + type: string + synchronizerId: + description: | + The synchronizer to which the contract is currently assigned. + If the contract is in the process of being reassigned, then a "409" response is returned. + type: string + debugPackageName: + description: | + The name of the Daml package that was used to create the contract. + Use this data only if you trust the provider, as it might not match the data in the + `createdEventBlob`. + type: string + debugPayload: + description: | + The contract arguments that were used to create the contract. + Use this data only if you trust the provider, as it might not match the data in the + `createdEventBlob`. + type: object + debugCreatedAt: + description: | + The ledger effective time at which the contract was created. + Use this data only if you trust the provider, as it might not match the data in the + `disclosedContract`. + type: string + format: date-time + required: + [ + "templateId", + "contractId", + "createdEventBlob", + "synchronizerId" + ] + + ErrorResponse: + type: object + required: + - error + properties: + error: + type: string diff --git a/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml b/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml new file mode 100644 index 0000000000..e88e446f18 --- /dev/null +++ b/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Description: Docker compose file for running Swagger UI with the transfer-instruction OpenAPI specification. +# Usage: docker-compose up +version: '3.7' + +services: + swagger-ui: + image: swaggerapi/swagger-ui + ports: + - "8080:8080" + environment: + SWAGGER_JSON: /spec/credential-registry.yaml + volumes: + - ./credential-registry.yaml:/spec/credential-registry.yaml From dd0d4f2ad6fdcc81faabde982c4bc0ac05683d14 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Tue, 16 Dec 2025 11:42:51 +0000 Subject: [PATCH 07/14] self-review --- .../daml/Splice/Ans.daml | 19 ++++--------------- .../daml/Splice/AnsV2/AnsCredential.daml | 5 ++--- .../Splice/Api/Credential/RegistryV1.daml | 6 +++--- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index 533639a19f..f30b650a31 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -288,9 +288,6 @@ template AnsEntryContext amuletSum = result.amuletSum - - - -- | A ans entry that needs to be renewed continuously. -- Renewal recreates this contract with an updated `expiresAt` field. template AnsEntry @@ -303,18 +300,6 @@ template AnsEntry expiresAt : Time where signatory user, dso -{- - - issuer: dso - holder: user - validFrom: None - validUntil: Some(expiresAt) - claims: - cns.canton.network/name: name - cns.canton.network//url: url - cns.canton.network//description: description - --} interface instance RegistryV1.CredentialRecord for AnsEntry where view = RegistryV1.CredentialRecordView with @@ -324,6 +309,10 @@ template AnsEntry credential = RegistryV1.Credential with claims = TM.fromList [ ("ans.name", name), + -- Make the URL and Description fields properties with the CNS name as the subject + -- Not namespaced as we expect them to be defined in a CIP + -- + -- TODO: build the separate CIP that builds ANS on top of the DSO credential registry. ("ans.url#" <> name, url), ("ans.description#" <> name, description) ] diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml index 158dd36113..f5e8869786 100644 --- a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml +++ b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml @@ -53,9 +53,7 @@ template AnsCredentialRegistry with newRecords meta = emptyMetadata - - --- | A credential visible to the DSO and served by its Scan service. +-- | A credential record visible to the DSO and served by its Scan service. template AnsCredentialRecord with dso : Party @@ -65,6 +63,7 @@ template AnsCredentialRecord createdAt : Time expiresAt : Time where + -- TODO: implement validation rules -- ensure validEntry this signatory issuer, holder diff --git a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml index c847617e91..cb94454db3 100644 --- a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml @@ -28,11 +28,11 @@ data Credential = Credential with -- Implementations SHOULD ensure that claims are stored in canonical form without -- redundant '#holder' suffixes in keys. -- - -- Keys MUST only contain characters from [a-zA-Z0-9._:-] - -- and SHOULD be namespaced in the form `dns.name/key` to avoid + -- Keys MUST only contain characters from [a-zA-Z0-9._:-]. + -- Unless the keys are defined in an official CIP they + -- must be namespaced in the form `dns.name/key` to avoid -- collisions between different usecases. - validFrom : Optional Time -- ^ The time from which this credential is valid. validUntil : Optional Time From 901ca941d0489bb7032a10fccfcb9f416b099f0b Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Fri, 9 Jan 2026 07:41:37 +0000 Subject: [PATCH 08/14] better naming for registry api --- build.sbt | 4 +- .../daml/Splice/Ans.daml | 8 +- .../daml/Splice/AnsV2/AnsCredential.daml | 32 ++++---- .../Splice/Api/Credential/RegistryV1.daml | 74 +++++++++++-------- 4 files changed, 67 insertions(+), 51 deletions(-) diff --git a/build.sbt b/build.sbt index 76a8044645..611ed2ccc9 100644 --- a/build.sbt +++ b/build.sbt @@ -874,7 +874,9 @@ lazy val `splice-amulet-name-service-daml` = .enablePlugins(DamlPlugin) .settings( BuildCommon.damlSettings, - Compile / damlDependencies := (`splice-wallet-payments-daml` / Compile / damlBuild).value, + Compile / damlDependencies := + (`splice-wallet-payments-daml` / Compile / damlBuild).value ++ + (`splice-api-credential-registry-v1-daml` / Compile / damlBuild).value, ) .dependsOn(`canton-bindings-java`) diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index f30b650a31..eb1df1c917 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -301,13 +301,13 @@ template AnsEntry where signatory user, dso - interface instance RegistryV1.CredentialRecord for AnsEntry where - view = RegistryV1.CredentialRecordView with + interface instance RegistryV1.Credential for AnsEntry where + view = RegistryV1.CredentialView with admin = dso issuer = dso holder = user - credential = RegistryV1.Credential with - claims = TM.fromList [ + claims = RegistryV1.Claims with + values = TM.fromList [ ("ans.name", name), -- Make the URL and Description fields properties with the CNS name as the subject -- Not namespaced as we expect them to be defined in a CIP diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml index f5e8869786..fe55918fd7 100644 --- a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml +++ b/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml @@ -21,12 +21,12 @@ template AnsCredentialRegistry with where signatory dso - interface instance RegistryV1.CredentialRegistry for AnsCredentialRegistry where - view = RegistryV1.CredentialRegistryView with + interface instance RegistryV1.CredentialFactory for AnsCredentialRegistry where + view = RegistryV1.CredentialFactoryView with admin = dso meta = emptyMetadata - credentialRegistry_updateRecordsImpl _self RegistryV1.CredentialRegistry_UpdateRecords{..} = do + credentialFactory_updateCredentialsImpl _self RegistryV1.CredentialFactory_UpdateCredentials{..} = do assertDeadlineExceeded "newCreatedAt" newCreatedAt -- TODO: make configurable let defaultExpiresAt = addRelTime newCreatedAt (days 90) -- default to 90 day expiry @@ -34,23 +34,23 @@ template AnsCredentialRegistry with -- check admin require "admin matches" (admin == dso) -- archive old records - forA_ oldRecords \oldRecordCid -> do - let ansRecordCid = fromInterfaceContractId @AnsCredentialRecord oldRecordCid - oldRecord <- fetchAndArchive (ForOwner with dso; owner = holder) ansRecordCid - require "issuer matches" (oldRecord.issuer == issuer) + forA_ oldCredentials \oldCredentialCid -> do + let ansCredentialCid = fromInterfaceContractId @AnsCredentialRecord oldCredentialCid + oldCredential <- fetchAndArchive (ForOwner with dso; owner = holder) ansCredentialCid + require "issuer matches" (oldCredential.issuer == issuer) -- create new records - newRecords <- forA newCredentials \newCredential -> do + newCredentials <- forA newCredentialClaims \claims -> do toInterfaceContractId <$> create AnsCredentialRecord with dso issuer holder - credential = newCredential + claims createdAt = newCreatedAt - expiresAt = optional defaultExpiresAt (min defaultExpiresAt) newCredential.validUntil + expiresAt = optional defaultExpiresAt (min defaultExpiresAt) claims.validUntil - pure RegistryV1.CredentialRegistry_UpdateRecordsResult with - newRecords + pure RegistryV1.CredentialFactory_UpdateCredentialsResult with + newCredentials meta = emptyMetadata -- | A credential record visible to the DSO and served by its Scan service. @@ -59,7 +59,7 @@ template AnsCredentialRecord dso : Party issuer : Party holder : Party - credential : RegistryV1.Credential + claims : RegistryV1.Claims createdAt : Time expiresAt : Time where @@ -69,12 +69,12 @@ template AnsCredentialRecord signatory issuer, holder observer dso - interface instance RegistryV1.CredentialRecord for AnsCredentialRecord where - view = RegistryV1.CredentialRecordView with + interface instance RegistryV1.Credential for AnsCredentialRecord where + view = RegistryV1.CredentialView with admin = dso issuer = issuer holder = holder - credential = credential + claims = claims createdAt = Some createdAt expiresAt = Some expiresAt meta = emptyMetadata diff --git a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml index cb94454db3..4bdaa66117 100644 --- a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml @@ -2,7 +2,8 @@ -- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 --- | Credential Registry API. +-- | Credential Registry API that defines both the schema of credentials and +-- the interface for managing credentials in a registry. module Splice.Api.Credential.RegistryV1 where import DA.TextMap (TextMap) @@ -10,13 +11,14 @@ import DA.TextMap (TextMap) import Splice.Api.Token.MetadataV1 --- | A credential modelled after the W3C Verifiable Credential data model. -data Credential = Credential with - claims : TextMap Text - -- ^ Claims are encoded as key-value pairs to align with the common use of - -- key-value pairs in ENS, DNS, k8s, and similar systems. A W3C claim - -- of the form (subject, property, value) is represented as a key-value pair - -- with the key formed as "property#subject" and value as "value". +-- | A set of claims that define a credential analogous to W3C Verifiable Credentials. +data Claims = Claims with + values : TextMap Text + -- ^ The values of the claims are encoded as key-value pairs to align with the + -- common use of key-value pairs in ENS, DNS, k8s, and similar systems. A + -- W3C claim of the form (subject, property, value) is represented as a + -- key-value pair with the key formed as "property#subject" and value as + -- "value". -- -- When no '#subject' suffix is present in the key, the claim pertains -- to the holder of the credential. Thus the key value pair @@ -28,64 +30,75 @@ data Credential = Credential with -- Implementations SHOULD ensure that claims are stored in canonical form without -- redundant '#holder' suffixes in keys. -- - -- Keys MUST only contain characters from [a-zA-Z0-9._:-]. + -- Keys MUST only contain characters from [a-zA-Z0-9._:#-] + -- and the '#' is only allowed to be used to separate property from subject. -- Unless the keys are defined in an official CIP they -- must be namespaced in the form `dns.name/key` to avoid -- collisions between different usecases. - validFrom : Optional Time -- ^ The time from which this credential is valid. validUntil : Optional Time -- ^ The time until which this credential is valid. meta : Metadata - -- ^ Metadata associated with this credential. Used for extensibility. + -- ^ Metadata associated with these claims. Used for extensibility. deriving (Eq, Show) -- | A view of a credential record stored in a credential registry. -data CredentialRecordView = CredentialRecordView with +data CredentialView = CredentialView with admin : Party -- ^ The party that administers this credential registry. issuer : Party -- ^ The party that issued the credential. holder : Party -- ^ The party that holds the credential. - credential : Credential + claims : Claims -- ^ The credential associated with this record. createdAt : Optional Time -- ^ The time at which this credential record was created. expiresAt : Optional Time - -- ^ The time at which this credential record expires. + -- ^ The time at which this credential record expires in the registry. + -- -- The registry MAY archive the record after this time. + -- + -- Separate from the `validUntil` field in `Claims`, as the expiry time of the + -- credential record in the registry is determined by the registry policy and + -- may differ from the validity period of the credential itself. meta : Metadata -- ^ Metadata associated with this credential record. Used for extensibility. deriving (Eq, Show) -- | A credential record stored in a credential registry. -interface CredentialRecord where - viewtype CredentialRecordView +-- +-- Note that there is intentionally no choice for fetching the credential publicly, so +-- either the holder or issuer must authorize usage of the credential record as part +-- of a Daml workflow. +interface Credential where + viewtype CredentialView -- Registry interface --------------------- --- | Credential registry interface. -interface CredentialRegistry where - viewtype CredentialRegistryView +-- | Credential factory interface to create, update, and archive credentials in the registry. +interface CredentialFactory where + viewtype CredentialFactoryView - credentialRegistry_updateRecordsImpl : ContractId CredentialRegistry -> CredentialRegistry_UpdateRecords -> Update CredentialRegistry_UpdateRecordsResult + credentialFactory_updateCredentialsImpl : ContractId CredentialFactory -> CredentialFactory_UpdateCredentials -> Update CredentialFactory_UpdateCredentialsResult - nonconsuming choice CredentialRegistry_UpdateRecords : CredentialRegistry_UpdateRecordsResult + nonconsuming choice CredentialFactory_UpdateCredentials : CredentialFactory_UpdateCredentialsResult -- ^ Update a set of credential records in the registry. with admin : Party -- ^ The admin of the credential records being updated. holder : Party -- ^ The holder of the credential records being updated. issuer : Party -- ^ The issuer of the credential records being updated. - oldRecords : [ContractId CredentialRecord] + oldCredentials : [ContractId Credential] -- ^ The existing credential records to archive. -- -- MAY be empty if no prior record exists. - newCredentials : [Credential] - -- ^ The new credentials to record. + + newCredentialClaims : [Claims] + -- ^ The claims of the new credentials to record. + newCreatedAt : Time -- ^ The creation time to use for the new credential records. -- @@ -100,18 +113,19 @@ interface CredentialRegistry where extraArgs : ExtraArgs -- ^ The extra arguments to pass to the implementation. controller holder, issuer - do credentialRegistry_updateRecordsImpl this self arg + do credentialFactory_updateCredentialsImpl this self arg --- | A view of a credential registry. -data CredentialRegistryView = CredentialRegistryView with +-- | A view of a credential factory contract for managing credentials in a registry. +data CredentialFactoryView = CredentialFactoryView with admin : Party -- ^ The party that administers this credential registry. meta : Metadata -- ^ Metadata associated with this credential registry. Used for extensibility. deriving (Eq, Show) -data CredentialRegistry_UpdateRecordsResult = CredentialRegistry_UpdateRecordsResult with - newRecords : [ContractId CredentialRecord] - -- ^ The newly created credential records. +data CredentialFactory_UpdateCredentialsResult = CredentialFactory_UpdateCredentialsResult with + newCredentials : [ContractId Credential] + -- ^ The newly created credential records for the provided claims. + -- Returned in the same order as the `newCredentialClaims` argument. meta : Metadata -- ^ Additional metadata specific to the update operation, used for extensibility. deriving (Eq, Show) From 8d02ed03c544abcea57a9271df1f8656d2f11632 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Fri, 9 Jan 2026 08:33:10 +0000 Subject: [PATCH 09/14] Polish DSO credential registry that is part of ANS --- .../daml/Splice/Ans.daml | 28 ++++- .../CredentialRegistry.daml} | 101 +++++++++++++----- .../Splice/Api/Credential/RegistryV1.daml | 2 +- 3 files changed, 102 insertions(+), 29 deletions(-) rename daml/splice-amulet-name-service/daml/Splice/{AnsV2/AnsCredential.daml => Ans/CredentialRegistry.daml} (53%) diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index eb1df1c917..f3e6a5af09 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -301,6 +301,11 @@ template AnsEntry where signatory user, dso + -- TODO: factor this instance out into a separate PR for the CIP that builds ANS + -- on top of the DSO credential registry. + -- + -- This instance is only provided here to test-drive the credential standard APIs. + -- interface instance RegistryV1.Credential for AnsEntry where view = RegistryV1.CredentialView with admin = dso @@ -308,13 +313,28 @@ template AnsEntry holder = user claims = RegistryV1.Claims with values = TM.fromList [ - ("ans.name", name), - -- Make the URL and Description fields properties with the CNS name as the subject - -- Not namespaced as we expect them to be defined in a CIP + ("ans.name#" <> name, partyToText user), + -- This claim translates to (subject="name", property="ans.name", value=holder). + -- + -- A name lookup matching ANS 1.0 lookup semantics + -- can be performed by searching for credentials with + -- key = "ans.name#" and issuer=dso + -- + -- TODO: consider saving space and defining the default rule that an empty value + -- means that the value is the holder party. It also obviates the need to deal + -- with credentials where the holder is different from the value of this claim. + -- + -- TODO: consider using reverse DNS style for the name, so that the prefix search + -- allows listing all names under a domain, e.g., "com.example.myname". -- - -- TODO: build the separate CIP that builds ANS on top of the DSO credential registry. ("ans.url#" <> name, url), ("ans.description#" <> name, description) + -- Note that name and description are associated with the ans.name and not + -- the holder directly; i.e., the holder can have different attributes for different names. + + -- TODO: consider whether it is time to remove the ANS -> CNS indirection + -- introduced to deal with trademark issues. At least for the properties in these + -- claims using `cns.` prefixes would reduce mental overhead. ] validFrom = None validUntil = Some expiresAt diff --git a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml b/daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml similarity index 53% rename from daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml rename to daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml index fe55918fd7..c884b4bcd9 100644 --- a/daml/splice-amulet-name-service/daml/Splice/AnsV2/AnsCredential.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml @@ -1,12 +1,15 @@ -- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 --- | Amulet Name Service (ANS) V2 Credential registry implementation. -module Splice.AnsV2.AnsCredential where +-- | Credential registry implementation provided as part of the Amulet Name Service (ANS) +-- run as port of the Decentralized Synchronizer Operations (DSO). +module Splice.Ans.CredentialRegistry where import DA.Assert import DA.Foldable (forA_) import DA.Time +import DA.Text as T +import DA.TextMap as TextMap import Splice.Types import Splice.Util @@ -15,7 +18,7 @@ import Splice.Api.Token.MetadataV1 (emptyMetadata) import Splice.Api.Credential.RegistryV1 qualified as RegistryV1 --- | Registry for creating ANS records. +-- | Credential registry factory contract. template AnsCredentialRegistry with dso : Party where @@ -33,7 +36,8 @@ template AnsCredentialRegistry with -- check admin require "admin matches" (admin == dso) - -- archive old records + + -- archive old credential records forA_ oldCredentials \oldCredentialCid -> do let ansCredentialCid = fromInterfaceContractId @AnsCredentialRecord oldCredentialCid oldCredential <- fetchAndArchive (ForOwner with dso; owner = holder) ansCredentialCid @@ -45,7 +49,7 @@ template AnsCredentialRegistry with dso issuer holder - claims + claims -- TODO: normalize claims by dropping redundant '#holder' suffixes createdAt = newCreatedAt expiresAt = optional defaultExpiresAt (min defaultExpiresAt) claims.validUntil @@ -63,8 +67,7 @@ template AnsCredentialRecord createdAt : Time expiresAt : Time where - -- TODO: implement validation rules - -- ensure validEntry this + ensure validCredentialRecord this signatory issuer, holder observer dso @@ -99,23 +102,73 @@ template AnsCredentialRecord pure () - -{- TODO: consider exact validation rules and implement them - - -validEntry : AnsCredential -> Bool -validEntry AnsCredential{..} = - TextMap.size claims <= 10 && - -- only printable ASCII characters in claim keys - -- at most 400 characters per claim key - -- at most 2000 characters per claim value - case (validFrom, validUntil) of - (Some from, Some until) -> createdAt <= from && from < until - (Some from, None) -> createdAt <= from - (None, Some until) -> createdAt < until - (None, None) -> True - --} +-- Credential record validation +------------------------------- + +-- | Credential record validation to ensure that the credentials are not too large and +-- can be efficiently indexed and processed by the Scan service. +-- +-- No content-level validation is performed, as that is application-specific and would +-- bind the registry to specific use cases. +validCredentialRecord : AnsCredentialRecord -> Bool +validCredentialRecord credential = + validClaims credential.createdAt credential.claims + -- no validation other than for the claims + +validClaims : Time -> RegistryV1.Claims -> Bool +validClaims createdAt claims = + -- at most 32 claims per credential to constrain indexing overhead + TextMap.size claims.values <= 32 && + -- claim values are valid + all validClaimValue (TextMap.toList claims.values) && + -- validity dates are consistent + case (claims.validFrom, claims.validUntil) of + (Some from, Some until) -> createdAt <= from && from < until + (Some from, None) -> createdAt <= from + (None, Some until) -> createdAt < until + (None, None) -> True + +validClaimValue : (Text, Text) -> Bool +validClaimValue (k, v) = + validKey k && + T.length v < 2048 + +-- | Keys are constrained to be < 512 characters, as they need to be indexed efficiently. +validKey : Text -> Bool +validKey k = + case T.splitOn "#" k of + [property] -> validProperty property + [property, subject] -> validProperty property && validSubject subject + _ -> False + +validProperty : Text -> Bool +validProperty property = + -- 254 chosen to ensure total key length is below 512 with subject suffixes + T.length property <= 254 && all validPropertyCodepoint (T.toCodePoints property) + +validSubject : Text -> Bool +validSubject subject = + -- 255 chosen to allow full party-ids + T.length subject <= 255 && all validPropertyCodepoint (T.toCodePoints subject) + +validPropertyCodepoint : Int -> Bool +validPropertyCodepoint c = + -- yeah, Daml does not have rgex support yet + (c >= 0x30 && c <= 0x39) -- '0' - '9' + || (c >= 0x41 && c <= 0x5A) -- 'A' - 'Z' + || (c >= 0x61 && c <= 0x7A) -- 'a' - 'z' + || c == 0x2E -- '.' + || c == 0x5F -- '_' + || c == 0x3A -- ':' + || c == 0x2D -- '-' + || c == 0x2F -- '/' + + +-- checked fetches +------------------ + +instance HasCheckedFetch AnsCredentialRegistry ForDso where + contractGroupId AnsCredentialRegistry{..} = ForDso with dso instance HasCheckedFetch AnsCredentialRecord ForOwner where contractGroupId AnsCredentialRecord{..} = ForOwner with dso; owner = holder diff --git a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml index 4bdaa66117..f9590bcaca 100644 --- a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml @@ -30,7 +30,7 @@ data Claims = Claims with -- Implementations SHOULD ensure that claims are stored in canonical form without -- redundant '#holder' suffixes in keys. -- - -- Keys MUST only contain characters from [a-zA-Z0-9._:#-] + -- Keys MUST only contain characters from [a-zA-Z0-9._:/#-] -- and the '#' is only allowed to be used to separate property from subject. -- Unless the keys are defined in an official CIP they -- must be namespaced in the form `dns.name/key` to avoid From 5a1f533a7a82a63f50015302f92245ee60eaa7b4 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Fri, 9 Jan 2026 09:19:42 +0000 Subject: [PATCH 10/14] polish existing endpoints in OpenAPI spec --- .../openapi/README.md | 11 ++ .../openapi/credential-registry-v1.yaml | 174 +++++++++++++----- .../openapi/docker-compose.yml | 4 +- 3 files changed, 139 insertions(+), 50 deletions(-) diff --git a/daml/splice-api-credential-registry-v1/openapi/README.md b/daml/splice-api-credential-registry-v1/openapi/README.md index 7f7b225cbc..d948434622 100644 --- a/daml/splice-api-credential-registry-v1/openapi/README.md +++ b/daml/splice-api-credential-registry-v1/openapi/README.md @@ -28,3 +28,14 @@ The fallback order can be used to page through all records in the order in which they were created; and it can also be used to tail the table to ingest all records as they are created. + +#### On deterministic responses when reading from multiple registry services + +The Scan proxy should implement a BFT read strategy where it queries +multiple scan endpoints. The following tricks might help to achieve +deterministic responses: + +- Determine a record_time cutoff based on discretizing record time + and choosing the end of a previous time slice to query up to. + + diff --git a/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml b/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml index 3196a5f581..91d0abeaa7 100644 --- a/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml +++ b/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml @@ -3,7 +3,7 @@ openapi: 3.0.0 info: - title: credential registry off-ledger API + title: Credential registry off-ledger API description: | Implemented by credential registries for the purpose of creating, updating, and querying credential records. @@ -15,40 +15,37 @@ paths: # TODO: add info endpoint where one can learn about limits of the registry etc. # TODO: consider renaming "CredentialRegistry" to "CredentialFactory" to be consistent with the token standard APIs; and to make the call below less confusing - /registry/credentials/v1/credential-registry + /registry/credentials/v1/credential-factory: post: - operationId: "getCredentialRegistry" + operationId: "getCredentialFactory" description: | - Get the credential registry contract and the choice context for updating records. + Get the credential factory contract and the choice context for updating credential records. requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/GetCredentialRegistryRequest" + $ref: "#/components/schemas/GetCredentialFactoryRequest" responses: "200": description: ok content: application/json: schema: - $ref: "#/components/schemas/CredentialsRegistryWithChoiceContext" + $ref: "#/components/schemas/CredentialFactoryWithChoiceContext" "400": $ref: "#/components/responses/400" "404": $ref: "#/components/responses/404" - /registry/credentials/v1/credentials + /registry/credentials/v1/credentials: get: operationId: "listCredentials" description: | List the credential records matching the provided filters. Note that registries MAY decide to limit the number of records returned. - - Decentralized registries MAY also store a record_time cutoff in the page token to make the - responses more stable from a pagination perspective. parameters: - in: query name: holder @@ -56,21 +53,25 @@ paths: type: string required: false description: | - The party that holds the credential records. + Only return credential records held by the specified party. - in: query name: issuer schema: - type: string + type: array + items: + type: string + style: form + explode: true required: false description: | - The party that issued the credential records. + Only return credential records issued by one of the specified issuers. - in: query name: keyPrefix schema: type: string required: false description: | - The prefix of the keys of the credential records. + Only return credential records that contain a claim with a key that starts with the given prefix. - in: query name: limit schema: @@ -79,13 +80,14 @@ paths: required: false description: | The maximum number of credential records to return. - - in: query - name: pageToken - schema: - type: string - required: false - description: | - The token for the page to retrieve. + - in: query + name: pageToken + schema: + type: string + required: false + description: | + The page token to continue retrieving results from a previous list request. + Ensure that the other filter parameters are identical to the previous request. - in: query name: validAsOf schema: @@ -95,10 +97,6 @@ paths: description: | If provided, only return credential records that are valid at the given time. - TODO: consider whether we can use a `validAsOf` date that is - slightly in the past to implement stable pagination for - decentralized registries. - - in: query name: includeDisclosedContracts schema: @@ -123,18 +121,7 @@ paths: content: application/json: schema: - type: object - properties: - credentials: - type: array - items: - $ref: "#/components/schemas/CredentialRecord" - description: | - The list of credential records matching the provided filters. - nextPageToken: - type: string - description: | - The token for the next page of results. Omitted if there are no more results. + $ref: "#/components/schemas/ListCredentialsResponse" "400": $ref: "#/components/responses/400" "404": @@ -158,35 +145,93 @@ components: $ref: "#/components/schemas/ErrorResponse" schemas: - GetCredentialRegistryRequest: - # TODO: fill out - - CredentialRecord: + GetCredentialFactoryRequest: type: object properties: - credentialRecordView: + choiceArguments: type: object description: | - The view computed for the credential record with the schema of Splice.Api.Credential.RegistryV1.CredentialRecordView. - disclosedContract: - $ref: "#/components/schemas/DisclosedContract" + The arguments that are intended to be passed to the choice provided by the factory. + To avoid repeating the Daml type definitions, they are specified as JSON objects. + However the concrete format is given by how the choice arguments are encoded using the Daml JSON API + (with the `extraArgs.context` and `extraArgs.meta` fields set to the empty object). + + The choice arguments are provided so that the registry can also make choice-argument + specific decisions, e.g., use different factories for different issuer or holder parties. + excludeDebugFields: + description: "If set to true, the response will not include fields prefixed with 'debug'. Useful to save bandwidth." + default: false + type: boolean + required: + [ + "choiceArguments", + ] + + CredentialFactoryWithChoiceContext: + description: | + The credential factory contract together with the choice context required to exercise the choice + provided by the factory. Typically used to implement the generic initiation of on-ledger workflows + via a Daml interface. + + Clients SHOULD avoid reusing the same `CredentialFactoryWithChoiceContext` for exercising multiple choices, + as the choice context MAY be specific to the choice being exercised. + type: object + properties: + factoryId: + description: "The contract ID of the contract implementing the factory interface." + type: string + choiceContext: + $ref: "#/components/schemas/ChoiceContext" + required: + [ + "factoryId", + "choiceContext", + "transferKind", + ] + + ChoiceContext: + description: | + The context required to exercise a choice on a contract via an interface. + Used to retrieve additional reference data that is passed in via disclosed contracts, + which are in turn referred to via their contract ID in the `choiceContextData`. + type: object + properties: + choiceContextData: + description: "The additional data to use when exercising the choice." + type: object + disclosedContracts: description: | - The disclosed contract information for verifying the credential record using explicit contract disclosure. + The contracts that are required to be disclosed to the participant node for exercising + the choice. + type: array + items: + $ref: "#/components/schemas/DisclosedContract" required: [ - "credentialRecordView", + "choiceContextData", + "disclosedContracts", ] - # Note: intentionally not shared with the other APIs to keep the self-contained, and because not all OpenAPI codegens support such shared definitions. DisclosedContract: type: object + description: | + A contract that is disclosed for use with explicit contract disclosure + when preparing transactions on validator nodes that do not know this contract. properties: templateId: type: string + description: "The template ID of the disclosed contract." contractId: type: string + description: "The contract ID of the disclosed contract." createdEventBlob: type: string + description: | + The binary-encoded created event of the disclosed contract. + This blob can be used to prepare transactions on validator + nodes that do not know this contract. This is done using + explicit contract disclosure, where the created event is + provided alongside the command that refers to the contract. synchronizerId: description: | The synchronizer to which the contract is currently assigned. @@ -219,6 +264,39 @@ components: "synchronizerId" ] + ListCredentialsResponse: + type: object + description: "The response for listing credential records." + properties: + credentials: + type: array + items: + $ref: "#/components/schemas/Credential" + description: | + The list of credential records matching the provided filters. + nextPageToken: + type: string + description: | + The token for the next page of results. Omitted if there are no more results. + + Credential: + type: object + description: | + A credential record stored in the registry. + properties: + credentialView: + type: object + description: | + The view computed for the credential with the schema of Splice.Api.Credential.RegistryV1.CredentialView. + disclosedContract: + $ref: "#/components/schemas/DisclosedContract" + description: | + The disclosed contract information for verifying the credential record using explicit contract disclosure. + required: + [ + "credentialView", + ] + ErrorResponse: type: object required: diff --git a/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml b/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml index e88e446f18..c26147c503 100644 --- a/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml +++ b/daml/splice-api-credential-registry-v1/openapi/docker-compose.yml @@ -11,6 +11,6 @@ services: ports: - "8080:8080" environment: - SWAGGER_JSON: /spec/credential-registry.yaml + SWAGGER_JSON: /spec/credential-registry-v1.yaml volumes: - - ./credential-registry.yaml:/spec/credential-registry.yaml + - ./credential-registry-v1.yaml:/spec/credential-registry-v1.yaml From 4eafd9fbbccb458269e5c33ae892b081cfa0e7e0 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Fri, 9 Jan 2026 09:44:14 +0000 Subject: [PATCH 11/14] add info endpoint --- .../openapi/credential-registry-v1.yaml | 88 +++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml b/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml index 91d0abeaa7..cc4810377d 100644 --- a/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml +++ b/daml/splice-api-credential-registry-v1/openapi/credential-registry-v1.yaml @@ -10,12 +10,24 @@ info: version: 1.0.0 paths: - # TODO: fix the schema validation errors. The current state is meant as a sketch to align on the design. - # TODO: add info endpoint where one can learn about limits of the registry etc. + /credential-registry/v1/info: + get: + operationId: "getCredentialRegistryInfo" + description: | + Get information about the credential registry. + Useful to discover the admin party and the supported versions of the credential registry standard. + responses: + "200": + description: ok + content: + application/json: + schema: + "$ref": "#/components/schemas/GetCredentialRegistryInfoResponse" + "404": + $ref: "#/components/responses/404" - # TODO: consider renaming "CredentialRegistry" to "CredentialFactory" to be consistent with the token standard APIs; and to make the call below less confusing - /registry/credentials/v1/credential-factory: + /credential-registry/v1/credential-factory: post: operationId: "getCredentialFactory" description: | @@ -39,7 +51,7 @@ paths: $ref: "#/components/responses/404" - /registry/credentials/v1/credentials: + /credential-registry/v1/credentials: get: operationId: "listCredentials" description: | @@ -145,6 +157,72 @@ components: $ref: "#/components/schemas/ErrorResponse" schemas: + GetCredentialRegistryInfoResponse: + type: object + properties: + adminId: + description: "The Daml party of the credential registry administrator" + type: string + supportedVersions: + description: "The versions of the credential registry standard supported by this registry" + type: array + items: + type: integer + format: int32 + limits: + description: "The limits enforced by the credential registry" + type: object + schema: + $ref: "#/components/schemas/CredentialRegistryLimits" + required: + - adminId + - supportedVersions + - limits + + CredentialRegistryLimits: + type: object + description: "The limits of the credential registry" + properties: + maxPageSize: + description: "The maximum page size supported by the registry for listing credential records" + type: integer + format: int32 + maxNumIssuersFilter: + description: "The maximum number of issuers that can be provided in the issuer filter when listing credential records" + type: integer + format: int32 + maxNumClaims: + description: | + The maximum number of claims allowed in new credential records. + There is no limit on the number of claims when querying existing records. + type: integer + format: int32 + maxPropertyLength: + description: | + The maximum length of a property key allowed in new credential records. + There is no limit on the property key length when querying existing records. + type: integer + format: int32 + maxSubjectLength: + description: | + The maximum length of the subject field allowed in new credential records. + There is no limit on the subject field length when querying existing records. + type: integer + format: int32 + maxValueLength: + description: | + The maximum length of a claim value allowed in new credential records. + There is no limit on the claim value length when querying existing records. + type: integer + format: int32 + required: + - maxPageSize + - maxNumIssuersFilter + - maxNumClaims + - maxPropertyLength + - maxSubjectLength + - maxValueLength + GetCredentialFactoryRequest: type: object properties: From 4567493f678e083af598f165aef2e47e688bba03 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Fri, 9 Jan 2026 14:00:03 +0000 Subject: [PATCH 12/14] add public-fetch and archive-as-holder --- .../daml/Splice/Ans.daml | 7 +++ .../daml/Splice/Ans/CredentialRegistry.daml | 9 +++ .../Splice/Api/Credential/RegistryV1.daml | 62 +++++++++++++++++-- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index f3e6a5af09..56e291a800 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -343,6 +343,13 @@ template AnsEntry expiresAt = Some expiresAt meta = emptyMetadata + credential_publicFetchImpl _self _arg = pure (view (toInterface @RegistryV1.Credential this)) + + credential_archiveAsHolderImpl _self _arg = do + pure RegistryV1.Credential_ArchiveAsHolderResult with + archivedCredential = view (toInterface @RegistryV1.Credential this) + meta = emptyMetadata + choice AnsEntry_Expire : AnsEntry_ExpireResult with actor : Party diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml b/daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml index c884b4bcd9..b9b8497c65 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans/CredentialRegistry.daml @@ -29,6 +29,8 @@ template AnsCredentialRegistry with admin = dso meta = emptyMetadata + credentialFactory_publicFetchImpl _self _arg = pure (view (toInterface @RegistryV1.CredentialFactory this)) + credentialFactory_updateCredentialsImpl _self RegistryV1.CredentialFactory_UpdateCredentials{..} = do assertDeadlineExceeded "newCreatedAt" newCreatedAt -- TODO: make configurable @@ -82,6 +84,13 @@ template AnsCredentialRecord expiresAt = Some expiresAt meta = emptyMetadata + credential_publicFetchImpl _self _arg = pure (view (toInterface @RegistryV1.Credential this)) + + credential_archiveAsHolderImpl _self _arg = do + pure RegistryV1.Credential_ArchiveAsHolderResult with + archivedCredential = view (toInterface @RegistryV1.Credential this) + meta = emptyMetadata + choice AnsCredential_Expire : () with actor : Party diff --git a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml index f9590bcaca..49e474ab4e 100644 --- a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml @@ -68,13 +68,48 @@ data CredentialView = CredentialView with deriving (Eq, Show) -- | A credential record stored in a credential registry. --- --- Note that there is intentionally no choice for fetching the credential publicly, so --- either the holder or issuer must authorize usage of the credential record as part --- of a Daml workflow. interface Credential where viewtype CredentialView + credential_archiveAsHolderImpl : ContractId Credential -> Credential_ArchiveAsHolder -> Update Credential_ArchiveAsHolderResult + credential_publicFetchImpl : ContractId Credential -> Credential_PublicFetch -> Update CredentialView + + choice Credential_ArchiveAsHolder : Credential_ArchiveAsHolderResult + -- ^ Archive this credential record as the holder. + -- + -- This is always allowed for the holder of the credential and matches the real-world analogue + -- of them destroying their copy of the credential. + -- + -- The view is returned for convenience so that the caller does not need to fetch it ahead of time. + controller (view this).holder + do credential_archiveAsHolderImpl this self arg + + nonconsuming choice Credential_PublicFetch : CredentialView + -- ^ Fetch the view of the credential. + -- + -- Registries MAY may restrict the actor in case the credential is not public. + with + expectedAdmin : Party + -- ^ The expected admin party storing the credential. Implementations MUST validate that this matches + -- the admin of the factory. + -- + -- Callers SHOULD ensure they get `expectedAdmin` from a trusted source, e.g., a read against + -- their own participant. That way they can ensure that it is safe to exercise a choice + -- on a factory contract acquired from an untrusted source *provided* + -- all vetted Daml packages only contain interface implementations + -- that check the expected admin party. + actor : Party + -- ^ The party fetching the contract. + controller actor + do credential_publicFetchImpl this self arg + +data Credential_ArchiveAsHolderResult = Credential_ArchiveAsHolderResult with + archivedCredential : CredentialView + -- ^ The view of the archived credential. + meta : Metadata + -- ^ Additional metadata specific to the archive operation, used for extensibility. + deriving (Eq, Show) + -- Registry interface --------------------- @@ -83,6 +118,7 @@ interface Credential where interface CredentialFactory where viewtype CredentialFactoryView + credentialFactory_publicFetchImpl : ContractId CredentialFactory -> CredentialFactory_PublicFetch -> Update CredentialFactoryView credentialFactory_updateCredentialsImpl : ContractId CredentialFactory -> CredentialFactory_UpdateCredentials -> Update CredentialFactory_UpdateCredentialsResult nonconsuming choice CredentialFactory_UpdateCredentials : CredentialFactory_UpdateCredentialsResult @@ -115,6 +151,24 @@ interface CredentialFactory where controller holder, issuer do credentialFactory_updateCredentialsImpl this self arg + nonconsuming choice CredentialFactory_PublicFetch : CredentialFactoryView + -- ^ Fetch the view of the factory contract. + with + expectedAdmin : Party + -- ^ The expected admin party issuing the factory. Implementations MUST validate that this matches + -- the admin of the factory. + -- + -- Callers SHOULD ensure they get `expectedAdmin` from a trusted source, e.g., a read against + -- their own participant. That way they can ensure that it is safe to exercise a choice + -- on a factory contract acquired from an untrusted source *provided* + -- all vetted Daml packages only contain interface implementations + -- that check the expected admin party. + actor : Party + -- ^ The party fetching the contract. + controller actor + do credentialFactory_publicFetchImpl this self arg + + -- | A view of a credential factory contract for managing credentials in a registry. data CredentialFactoryView = CredentialFactoryView with From 286070e0f27dae0e15f6689de60029a59efcc2d5 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Mon, 12 Jan 2026 12:29:47 +0000 Subject: [PATCH 13/14] add credentials for featured app rights --- build.sbt | 3 +- daml/splice-amulet/daml.yaml | 1 + daml/splice-amulet/daml/Splice/Amulet.daml | 45 +++++++++++++++++----- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index 611ed2ccc9..92df79f647 100644 --- a/build.sbt +++ b/build.sbt @@ -702,7 +702,8 @@ lazy val `splice-amulet-daml` = (`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-request-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-credential-registry-v1-daml` / Compile / damlBuild).value, ) .dependsOn(`canton-bindings-java`) diff --git a/daml/splice-amulet/daml.yaml b/daml/splice-amulet/daml.yaml index d0edcb5cbb..ef8d1e6f4c 100644 --- a/daml/splice-amulet/daml.yaml +++ b/daml/splice-amulet/daml.yaml @@ -18,6 +18,7 @@ data-dependencies: - ../../token-standard/splice-api-token-allocation-instruction-v1/.daml/dist/splice-api-token-allocation-instruction-v1-current.dar - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-api-featured-app-v1/.daml/dist/splice-api-featured-app-v1-current.dar + - ../splice-api-credential-registry-v1/.daml/dist/splice-api-credential-registry-v1-current.dar build-options: - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/daml/splice-amulet/daml/Splice/Amulet.daml b/daml/splice-amulet/daml/Splice/Amulet.daml index d3366d4190..2b0df20e82 100644 --- a/daml/splice-amulet/daml/Splice/Amulet.daml +++ b/daml/splice-amulet/daml/Splice/Amulet.daml @@ -13,6 +13,7 @@ import DA.Optional (fromOptional) import Splice.Api.Token.MetadataV1 qualified as Api.Token.MetadataV1 import Splice.Api.Token.HoldingV1 qualified as Api.Token.HoldingV1 +import Splice.Api.Credential.RegistryV1 qualified as Api.Credential.RegistryV1 import Splice.Amulet.TokenApiUtils import Splice.Expiry @@ -81,7 +82,7 @@ data SvRewardCoupon_ArchiveAsBeneficiaryResult = SvRewardCoupon_ArchiveAsBenefic data UnclaimedActivityRecord_ArchiveAsBeneficiaryResult = UnclaimedActivityRecord_ArchiveAsBeneficiaryResult -data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with +data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid : ContractId UnclaimedReward -- | A amulet, which can be locked and whose amount expires over time. @@ -246,6 +247,30 @@ template FeaturedAppRight with weight = weight pure (Splice.Api.FeaturedAppRightV1.FeaturedAppRight_CreateActivityMarkerResult $ map toInterfaceContractId cids) + interface instance Api.Credential.RegistryV1.Credential for FeaturedAppRight where + view = Api.Credential.RegistryV1.CredentialView with + admin = dso + issuer = dso + holder = provider + claims = Api.Credential.RegistryV1.Claims with + values = TextMap.fromList [ + ("cip-TBD/is-featured-app", "") + -- TODO: replace TBD with the actual CIP number once it is assigned; factor it out into a constant + ] + validFrom = None + validUntil = None + meta = Api.Token.MetadataV1.emptyMetadata + createdAt = None + expiresAt = None + meta = Api.Token.MetadataV1.emptyMetadata + + credential_publicFetchImpl _self _arg = pure (view (toInterface @Api.Credential.RegistryV1.Credential this)) + + credential_archiveAsHolderImpl _self _arg = do + pure Api.Credential.RegistryV1.Credential_ArchiveAsHolderResult with + archivedCredential = view (toInterface @Api.Credential.RegistryV1.Credential this) + meta = Api.Token.MetadataV1.emptyMetadata + validateAppRewardBeneficiaries : [Splice.Api.FeaturedAppRightV1.AppRewardBeneficiary] -> Update () validateAppRewardBeneficiaries beneficiaries = do -- Note: we limit the minimal weight to limit the management overhead of the SVs @@ -392,29 +417,29 @@ template UnclaimedReward with signatory dso --- | A record of activity that can be minted by the beneficiary. --- Note that these do not come out of the per-round issuance but are instead created by burning --- UnclaimedRewardCoupon as defined through a vote by the SVs. That's also why expiry is a separate +-- | A record of activity that can be minted by the beneficiary. +-- Note that these do not come out of the per-round issuance but are instead created by burning +-- UnclaimedRewardCoupon as defined through a vote by the SVs. That's also why expiry is a separate -- time-based expiry instead of being tied to a round like the other activity records. template UnclaimedActivityRecord with dso : Party beneficiary : Party -- ^ The owner of the `Amulet` to be minted. amount : Decimal -- ^ The amount of `Amulet` to be minted. - reason : Text -- ^ A reason to mint the `Amulet`. - expiresAt : Time -- ^ Selected timestamp defining the lifetime of the contract. - where + reason : Text -- ^ A reason to mint the `Amulet`. + expiresAt : Time -- ^ Selected timestamp defining the lifetime of the contract. + where signatory dso observer beneficiary ensure amount > 0.0 choice UnclaimedActivityRecord_DsoExpire : UnclaimedActivityRecord_DsoExpireResult controller dso - do + do assertDeadlineExceeded "UnclaimedActivityRecord.expiresAt" expiresAt unclaimedRewardCid <- create UnclaimedReward with dso; amount pure UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid - + requireAmuletExpiredForAllOpenRounds : ContractId OpenMiningRound -> Amulet -> Update () requireAmuletExpiredForAllOpenRounds roundCid amulet = do @@ -456,4 +481,4 @@ instance HasCheckedFetch FeaturedAppActivityMarker ForDso where contractGroupId FeaturedAppActivityMarker {..} = ForDso with dso instance HasCheckedFetch UnclaimedActivityRecord ForOwner where - contractGroupId UnclaimedActivityRecord{..} = ForOwner with dso; owner = beneficiary \ No newline at end of file + contractGroupId UnclaimedActivityRecord{..} = ForOwner with dso; owner = beneficiary From ea216e56403284b50b0601d9ce110fc7b1d49cb0 Mon Sep 17 00:00:00 2001 From: Simon Meier Date: Mon, 12 Jan 2026 12:40:18 +0000 Subject: [PATCH 14/14] switch to using ! as the property!subject separator [no ci] --- .../daml/Splice/Ans.daml | 15 +++++------ .../Splice/Api/Credential/RegistryV1.daml | 27 ++++++++++++------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index 56e291a800..454fbb5a30 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -313,12 +313,14 @@ template AnsEntry holder = user claims = RegistryV1.Claims with values = TM.fromList [ - ("ans.name#" <> name, partyToText user), - -- This claim translates to (subject="name", property="ans.name", value=holder). + -- TODO: use the right CIP number once it is assigned; factor it out into a constant + + ("cip-TBD/cns.name!" <> name, partyToText user), + -- This claim translates to (subject="name", property="cip-TBD/cns.name", value=holder). -- -- A name lookup matching ANS 1.0 lookup semantics -- can be performed by searching for credentials with - -- key = "ans.name#" and issuer=dso + -- key = "cip-TBD/cns.name#" and issuer=dso -- -- TODO: consider saving space and defining the default rule that an empty value -- means that the value is the holder party. It also obviates the need to deal @@ -327,14 +329,11 @@ template AnsEntry -- TODO: consider using reverse DNS style for the name, so that the prefix search -- allows listing all names under a domain, e.g., "com.example.myname". -- - ("ans.url#" <> name, url), - ("ans.description#" <> name, description) + ("cip-TBD/cns.url!" <> name, url), + ("cip-TBD/cns.description!" <> name, description) -- Note that name and description are associated with the ans.name and not -- the holder directly; i.e., the holder can have different attributes for different names. - -- TODO: consider whether it is time to remove the ANS -> CNS indirection - -- introduced to deal with trademark issues. At least for the properties in these - -- claims using `cns.` prefixes would reduce mental overhead. ] validFrom = None validUntil = Some expiresAt diff --git a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml index 49e474ab4e..fa8af4c7d3 100644 --- a/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml +++ b/daml/splice-api-credential-registry-v1/daml/Splice/Api/Credential/RegistryV1.daml @@ -17,24 +17,31 @@ data Claims = Claims with -- ^ The values of the claims are encoded as key-value pairs to align with the -- common use of key-value pairs in ENS, DNS, k8s, and similar systems. A -- W3C claim of the form (subject, property, value) is represented as a - -- key-value pair with the key formed as "property#subject" and value as + -- key-value pair with the key formed as "property!subject" and value as -- "value". -- - -- When no '#subject' suffix is present in the key, the claim pertains + -- When no '!subject' suffix is present in the key, the claim pertains -- to the holder of the credential. Thus the key value pair -- - -- "profile.displayName" : "Alice" + -- "cip-TBD/displayName" : "Alice" -- - -- corresponds to the W3C claim (subject=holder, property="profile.displayName", value="Alice"). + -- corresponds to the W3C claim + -- + -- (subject=holder, property="cip-TBD/displayName", value="Alice"). -- -- Implementations SHOULD ensure that claims are stored in canonical form without - -- redundant '#holder' suffixes in keys. + -- redundant '!holder' suffixes in keys. + -- + -- Keys MUST only contain characters from [a-zA-Z0-9._:/!-] + -- and the '!' is only allowed to be used to separate property from subject. + -- + -- All keys MUST be namespaced in the form `namespace/property' to avoid collisions. + -- The namespace `cip-` is reserved for CIP-defined properties. + -- All other applications MUST use a Java-style reverse DNS name for + -- a domain under their control. -- - -- Keys MUST only contain characters from [a-zA-Z0-9._:/#-] - -- and the '#' is only allowed to be used to separate property from subject. - -- Unless the keys are defined in an official CIP they - -- must be namespaced in the form `dns.name/key` to avoid - -- collisions between different usecases. + -- For example, a key for a property `prop` defined by the domain + -- `example.com` would be `com.example/prop`. validFrom : Optional Time -- ^ The time from which this credential is valid. validUntil : Optional Time