Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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)
Expand All @@ -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"))
Expand All @@ -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`)

Expand Down Expand Up @@ -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`)

Expand All @@ -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`)

Expand Down Expand Up @@ -857,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`)

Expand Down Expand Up @@ -922,7 +941,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(
Expand Down
3 changes: 3 additions & 0 deletions daml/splice-amulet-name-service/daml.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ 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
Expand Down
53 changes: 53 additions & 0 deletions daml/splice-amulet-name-service/daml/Splice/Ans.daml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module Splice.Ans where

import DA.Action (void)
import DA.TextMap qualified as TM
import DA.Time

import Splice.Amulet
Expand All @@ -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
Expand Down Expand Up @@ -297,6 +301,55 @@ 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
issuer = dso
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).
--
-- A name lookup matching ANS 1.0 lookup semantics
-- can be performed by searching for credentials with
-- key = "ans.name#<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".
--
("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
meta = emptyMetadata
createdAt = None
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

-- | 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

import Splice.Api.Token.MetadataV1 (emptyMetadata)
import Splice.Api.Credential.RegistryV1 qualified as RegistryV1


-- | Credential registry factory contract.
template AnsCredentialRegistry with
dso : Party
where
signatory dso

interface instance RegistryV1.CredentialFactory for AnsCredentialRegistry where
view = RegistryV1.CredentialFactoryView 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
let defaultExpiresAt = addRelTime newCreatedAt (days 90) -- default to 90 day expiry

-- check admin
require "admin matches" (admin == dso)

-- archive old credential records
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
newCredentials <- forA newCredentialClaims \claims -> do
toInterfaceContractId <$> create AnsCredentialRecord with
dso
issuer
holder
claims -- TODO: normalize claims by dropping redundant '#holder' suffixes
createdAt = newCreatedAt
expiresAt = optional defaultExpiresAt (min defaultExpiresAt) claims.validUntil

pure RegistryV1.CredentialFactory_UpdateCredentialsResult with
newCredentials
meta = emptyMetadata

-- | A credential record visible to the DSO and served by its Scan service.
template AnsCredentialRecord
with
dso : Party
issuer : Party
holder : Party
claims : RegistryV1.Claims
createdAt : Time
expiresAt : Time
where
ensure validCredentialRecord this

signatory issuer, holder
observer dso

interface instance RegistryV1.Credential for AnsCredentialRecord where
view = RegistryV1.CredentialView with
admin = dso
issuer = issuer
holder = holder
claims = claims
createdAt = Some createdAt
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
controller actor
do
require "actor is stakeholder" (actor == holder || actor == issuer || actor == dso)
assertDeadlineExceeded "expiresAt" expiresAt

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 ()


-- 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
18 changes: 18 additions & 0 deletions daml/splice-api-credential-registry-v1/daml.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading