panurus

Identity Service

The Identity Service (token/services/identity) is an internal infrastructure service of Panurus. It provides a unified interface for managing identities, signatures, and verification, operating independently of the core Fabric Smart Client (FSC) identity service.

This independence ensures that token-related cryptographic material (such as Idemix pseudonyms or X.509 certificates used for token ownership) is managed according to the specific privacy and security requirements of the Token Drivers, regardless of the underlying DLT platform.

Overview

The Identity Service abstracts the complexity of different cryptographic schemes, allowing Panurus to support multiple identity types (e.g., X.509, Idemix) and different storage backends seamlessly.

It is a fundamental component used by token drivers and application services (like the TTX service) to handle:

Architecture

The Identity Service implements the Driver API interfaces defined in token/driver/wallet.go. This ensures that the Token Management System (TMS) can interact with any identity implementation through a standard set of methods.

Component Mapping

The following table shows how the internal components map to the Driver API interfaces:

Component Implements Driver Interface Description
identity.Provider driver.IdentityProvider Core identity management & verification.
wallet.Service driver.WalletService Registry for all wallets (Owner, Issuer, etc.).
role.LongTermOwnerWallet driver.OwnerWallet Long-Term Identity-based Owner wallet functionality.
role.AnonymousOwnerWallet driver.OwnerWallet Anonymous Identity-based Owner wallet functionality.
role.IssuerWallet driver.IssuerWallet Issuer wallet functionality.
role.AuditorWallet driver.AuditorWallet Auditor wallet functionality.
role.CertifierWallet driver.CertifierWallet Certifier wallet functionality.

Component Interaction

classDiagram
    direction TB
%% Driver Interfaces
    class IdentityProvider {
        <<interface>>
        +GetSigner()
        +GetAuditInfo()
        +IsMe()
    }
    class WalletService {
        <<interface>>
        +OwnerWallet()
        +IssuerWallet()
        +RegisterRecipientIdentity()
    }

%% Concrete Implementations
    class identity_Provider["identity.Provider"] {
        -Storage
        -Deserializers
        -SignerCache
    }
    class wallet_Service["wallet.Service"] {
        -RoleRegistry
        -IdentityProvider
        -OwnerWallet
        -IssuerWallet
        -AuditorWallet
        -CertifierWallet
    }
    class role_Role["role.Role"] {
        -LocalMembership
        +GetIdentityInfo()
    }
    class membership_KeyManagerProvider["membership.KeyManagerProvider"] {
        <<interface>>
        +Get() KeyManager
    }

    identity_Provider ..|> IdentityProvider : Implements
    wallet_Service ..|> WalletService : Implements
    wallet_Service --> identity_Provider : Uses
    wallet_Service --> role_Role : Uses (via RoleRegistry)
    role_Role --> membership_KeyManagerProvider : Uses (via LocalMembership)

    note for membership_KeyManagerProvider "Handles low-level crypto<br/>and identity verification"
    note for wallet_Service "High-level management<br/>of wallets and roles"

Wallet Registry concurrency

role.Registry (token/services/identity/role/registry.go) is the per-role wallet cache behind wallet.Service. Its contract:

A WalletFactory implementation must therefore be safe to call concurrently for distinct wallet identifiers, and may assume no registry lock is held when it is invoked.

A wallet identifier registered with a nil wallet counts as absent, both on the fast path and when creation double-checks the cache; a factory that returns no wallet and no error is reported as an error.

LocalMembership

The LocalMembership component (token/services/identity/membership) plays a pivotal role in managing local identities for a specific role (e.g., Owner, Issuer).

Example: Wiring Services

The following example demonstrates how these services are instantiated and wired together, as seen in the ZKATDLog driver:

func (d *Base) NewWalletService(...) (*wallet.Service, error) {
    // 1. Create Identity Provider
    identityProvider := identity.NewProvider(...)

    // 2. Initialize Membership Role Factory
    roleFactory := membership.NewRoleFactory(...)

    // 3. Configure Key Managers (e.g. Idemix and X.509 for Owner role)
    // we have one key manager to handle fabtoken tokens and one for each idemix issuer public key in the public parameters
    kmps := make([]membership.KeyManagerProvider, 0)
    // ... add Idemix Key Manager Providers ...
    kmps = append(kmps, x509.NewKeyManagerProvider(...))

    // 4. Create and Register Roles
    roles := role.NewRoles()
    
    // Owner Role (with anonymous identities)
    ownerRole, err := roleFactory.NewRole(identity.OwnerRole, true, nil, kmps...)
    roles.Register(identity.OwnerRole, ownerRole)
    
    // Issuer Role (no anonymous identities)
    issuerRole, err := roleFactory.NewRole(identity.IssuerRole, false, pp.Issuers(), x509.NewKeyManagerProvider(...))
    roles.Register(identity.IssuerRole, issuerRole)
    
    // ... Register Auditor and Certifier roles ...

    // 5. Create Wallet Service with the registered roles
    return wallet.NewService(
        logger,
        identityProvider,
        deserializer,
        // Convert the roles registry into the format expected by the wallet service
        wallet.Convert(roles.Registries(...)),
    ), nil
}

SignerRouter (conf_id-pinned fast path)

GetSigner’s default resolution path is a fallback deserializer: a linear scan across every KeyManager registered under the identity’s type, each probed with a cryptographic sign+verify to find the one that actually matches. SignerRouter (token/services/identity/signer_router.go) is an optional fast path that skips this scan-and-probe entirely: it resolves the conf_id an identity was bound under (via a ConfIDResolver) and dispatches straight to the single KeyManager registered for that conf_id.

conf_id must be collision-free

Because the probe is skipped, routing correctness rests entirely on conf_id identifying exactly one configuration. A conf_id is minted by AddConfiguration from driver.IdentityConfiguration.UniqueID() — the hash of CompositeKey(), an encoding of the (ID, Type, URL) tuple (token/driver/wallet.go) — and thereafter lives in identity_configurations.conf_id. That encoding joins the three fields with @ and escapes any @ or \ occurring inside a field, so distinct tuples always produce distinct keys.

The escaping is what makes the encoding injective. Joining the fields unescaped would let {ID: "a@b", Type: "c"} and {ID: "a", Type: "b@c"} produce the same conf_id; SignerRouter.byConfID would then hold a single entry for the two configurations and hand identities of one to the other’s KeyManager — with the probe skipped, nothing detects it, and it surfaces later as invalid signatures rather than as a routing error.

Three consequences worth knowing:

Metrics

identity.Metrics (token/services/identity/metrics.go) instruments both Provider.GetSigner and SignerRouter, sharing one Metrics instance built with identity.NewMetrics(provider) (a nil provider yields a disabled.Provider-backed noop):

Metric Type Labels Purpose
identity_signer_resolutions_total Counter network, channel, namespace, outcome = cache | routed | fallback How each GetSigner call was ultimately resolved.
identity_get_signer_duration_seconds Histogram network, channel, namespace, path = cache | routed | fallback GetSigner wall-clock time by resolution path; compares the latency saved by skipping the probe.
identity_signer_router_registrations_total Counter network, channel, namespace conf_id→KeyManager bindings registered with the SignerRouter. A near-zero count in production means routing is never populated and every call falls back.
identity_signer_router_no_probe_errors_total Counter network, channel, namespace Failures of the probe-free deserialization path — since that path skips the cryptographic check, a non-zero count is worth investigating as a conf_id routing bug.

Note: provider here is a NewTMSProvider-wrapped Provider (see Driver Metrics), which binds network/channel/namespace on every metric via .With(...) before returning it. Every CounterOpts/HistogramOpts above must therefore declare those three as LabelNames in addition to its own label(s), or the metric panics with “inconsistent label cardinality” on first use. This is exactly the bug that crashed the DVP/DLog integration suite in SignerRouter.Register before it was fixed.

Wallet Lifecycle and Recipient Data Caching

Anonymous owner wallets hand out a fresh pseudonym for every payment. Generating one is expensive (an Idemix pseudonym plus a registry binding), so AnonymousOwnerWallet keeps a pre-provisioned buffer of recipient data. Two caches implement that buffer:

Cache Buffers Sized by
role.RecipientDataCache (token/services/identity/role/cache.go) driver.RecipientData (pseudonym + audit info) for one wallet wallets.owners[].cacheSize, falling back to wallets.defaultCacheSize (see configuration)
idemix/cache.IdentityCache (token/services/identity/idemix/cache/cache.go) idriver.IdentityDescriptor for one Idemix key manager same lookup, via KeyManagerProvider.cacheSizeForID

Both follow the same contract:

Cache metrics

Metric Type Cache Purpose
recipient_data_cache_level Gauge RecipientDataCache Entries currently buffered. Counted only once an entry is really in the buffer, so it cannot drift upward when the producer is blocked.
recipient_data_provision_failures_total Counter RecipientDataCache Failed pre-provisioning attempts. A rising rate means the identity backend is failing and requests are falling back to the slower on-demand path.
cache_level Gauge idemix IdentityCache As above, for Idemix identities.
cache_provision_failures_total Counter idemix IdentityCache As above, for Idemix identities.

Note: these providers are NewTMSProvider-wrapped, so every GaugeOpts/CounterOpts above must declare network, channel and namespace in LabelNames — omitting them panics with “inconsistent label cardinality” on first use. See Driver Metrics.

Who calls Close()

Application code does not normally close these caches itself: they are released by the existing teardown chain when a token management service is unloaded, for instance when its public parameters are updated.

core.TMSProvider.Update            (token/core/tms.go)
  └── Service.Done()               (token/core/common/tms.go)
        └── wallet.Service.Done()  (token/services/identity/wallet/service.go)
              └── role.Registry.Done()
                    ├── Close() on every wallet it created that holds resources
                    │     └── AnonymousOwnerWallet.Close() → RecipientDataCache.Close()
                    └── Role.Done() → LocalMembership.Close()

LocalMembership.Close() is best-effort: it releases its key managers, then unsubscribes from the identity store’s change notifier. If the store cannot supply a notifier — because it does not support one (storage.ErrNotSupported) or because it fails outright — the unsubscribe step is skipped and, in the failure case, logged. Close() returns no error and must never panic, since it runs on the shutdown path of an already-degraded node.

role.Registry.Done() closes wallets through a local interface{ Close() } assertion rather than through driver.Wallet, so wallet types with nothing to release need not implement a no-op Close(). If you add a wallet type that owns a goroutine, a ticker or any other resource, give it a Close() method and it will be released automatically.

Note: tests that exercise an anonymous owner wallet should t.Cleanup(w.Close), otherwise each test leaves a provisioning goroutine behind for the rest of the run.

Identity Registration Checks

Every path that binds something to an identity — audit info, token metadata, signer info, a signer — first requires the identity to be non-empty, and refuses it outright otherwise. This is not input tidying. Identity rows and the provider’s caches are keyed by Identity.UniqueID(), which maps the empty identity to the literal string <empty> rather than to a hash, so every empty identity would collapse onto one row and one cache entry: one caller’s audit info would be readable by any other empty-identity lookup, and a signer registered for one would be handed back for another. The guard therefore also applies to ephemeral registrations, which write nothing to storage but populate the same caches.

Two further checks apply where the information to make them exists:

On the read side, GetAuditInfo, GetTokenInfo, and (on the SQL backend) GetSignerInfo locate a row by identity hash and then compare the stored identity against the requested one before returning or caching anything, so a hash-addressed read cannot silently return another identity’s data. For the full posture, including the KVS backend’s inability to make the last comparison, see Store Integrity Verification.

Identity Types

The Identity Service leverages a wrapper called TypedIdentity to support various identity schemes uniformly. This allows Panurus to be extensible and capable of handling different cryptographic requirements.

TypedIdentity

TypedIdentity (defined in token/services/identity/typed.go) acts as a generic container. It wraps the raw identity bytes with a type label, enabling the system to verify deserializers and process signatures correctly without hardcoding implementation details.

Canonical encoding requirement

The DER envelopes of an identity must be the canonical encoding of the value they decode to — exactly one byte string per logical envelope:

The reason is Identity.UniqueID(): it hashes the raw identity bytes rather than a canonicalised form of the decoded value, and it is the cache key throughout the identity and wallet layers (role/registry.go’s fast-path cache, provider.go’s signer cache, and so on). Any two byte strings that decode to the same logical identity but hash differently give that one identity two cache slots — a token paid to the second spelling still verifies, because verification works on the decoded value, but never resolves to its owner’s wallet, because the lookup works on UniqueID(). Both producers of these bytes — appendTLV in the marshal package and encoding/asn1.Marshal for legacy encodings — already emit minimal lengths, minimal integers and no undeclared elements, so the stricter decode rejects nothing this tree ever writes. That last statement is about data written by this encoder — see Upgrade considerations below for bytes that are already persisted.

What this does not cover. The guarantee is about the envelopes, not about everything reachable through them:

Upgrade considerations. DecodeIdentity runs on every owner identity during validation, and MultiSignature/PolicySignature.FromBytes run inside Verifier.Verify, so tightening them changes which transactions are valid. The tightening is deliberately ungated — there is no public-parameters or capability flag for it, matching the precedent set by the nesting-depth and fan-out limits added alongside it. Two consequences follow from that choice:

This applies to identity decoding only. Signature parsing (x509/crypto/ecdsa.go, idemixnym/nym/signer.go) stays deliberately lenient: those bytes come from external signers and HSMs whose DER encoders are routinely non-minimal in ways that are still valid for signature purposes. The MultiSignature / PolicySignature envelopes are strict, because we always produce them ourselves; the individual signatures they carry are not.

Default Key Managers

The identity service includes two primary implementations for concrete identities:

1. X.509

Standard PKIX identities.

Expected Folder Structure

The X.509 Key Manager expects a specific folder structure when loading configurations from a local directory. It supports loading public signing certificates and, optionally, private keys for signing capabilities.

Directory Structure

The cryptographic materials are stored in standard PEM format. By default, the directory layout is as follows:

<dir>/
├── signcerts/
│   └── <cert>.pem          # Public signing certificate (X.509 PEM format)
└── keystore/
    └── priv_sk             # (Optional) Private key file (PEM format)
Detailed Structure Components
Custom Key Store Directory

While keystore is the default directory name for the private key, a custom keystore directory name can be passed as an argument when initializing the key manager (e.g. to load priv_sk from <dir>/<custom-keystore-name>/priv_sk).

2. Idemix (Identity Mixer)

Advanced identity encryption based on Zero-Knowledge Proofs (ZKP).

Expected Folder Structure

The Idemix Key Manager expects a specific folder structure when loading configurations from a local directory. It supports two different formats for cryptographic configurations:

1. Standard Idemix Format (Protobuf)

In this format, cryptographic materials are stored in binary protobuf format (generated by idemixgen). The directory structure is as follows:

<dir>/
├── msp/
│   └── IssuerPublicKey      # Issuer Public Key (binary protobuf)
└── user/
    ├── SignerConfig         # Signer configuration (binary protobuf)
    └── SignerConfigFull     # (Optional) Full signer config with secret keys

[!NOTE] SignerConfigFull is checked first and used if it exists when the service is configured to force the load of secret keys (i.e. ignoreVerifyOnlyWallet is set to true).

2. Fabric-CA Idemix Format (JSON)

In this format (typically generated by Fabric-CA), the signer configuration is stored as a JSON file:

<dir>/
├── msp/
│   └── IssuerPublicKey      # Issuer Public Key (binary protobuf)
└── user/
    └── SignerConfig         # Signer configuration (JSON format)
Directory Path Fallback

To accommodate different deployment structures, the Key Manager performs directory resolution using a fallback strategy:

  1. It first attempts to load the files directly from the configured directory (<dir>).
  2. If this fails, it appends an extra msp path element to the directory (i.e., <dir>/msp/) and tries again (e.g. searching for <dir>/msp/msp/IssuerPublicKey and <dir>/msp/user/SignerConfig).
Credential Verification at Load Time

When the loaded signer configuration carries secret key material (user secret key plus credential), the Idemix Key Manager verifies the credential against the issuer public key while it is being constructed. A credential that does not verify — whether the underlying BCCSP reports the failure as an error or simply as a negative verification result — makes construction fail with credential is not cryptographically valid; no key manager is returned. Configurations without secret key material are loaded as verify-only (remote) key managers and skip this check.

3. IdemixNym (Idemix with Pseudonym-based Identity)

An extension of Idemix that uses a commitment to the Enrollment ID (EID) as the identity instead of the full Idemix signature.

Key Differences from Standard Idemix:

Aspect Idemix IdemixNym
Identity (Token Owner) Full Idemix signature with attributes Nym EID (commitment to enrollment ID)
Identity Payload Encoding Protobuf Raw bytes
Audit Info Encoding JSON JSON (extended)
Signature Encoding Raw bytes ASN.1 (Creator + Signature)
Identity Size Large (~several KB) Small (~32-64 bytes)
Storage Overhead High Low

Audit Info Deserialization (Idemix and IdemixNym)

Audit info is JSON and can arrive from a counterparty (recipient registration, auditing flows), so both crypto.AuditInfo.FromBytes (token/services/identity/idemix/crypto/audit.go) and nym.AuditInfo.FromBytes (token/services/identity/idemixnym/nym/audit.go) treat their input as untrusted and reject malformed payloads with an error.

Both decode through the project’s strict JSON wrapper (token/core/common/encoding/json, which sets DisallowUnknownFields) — the same logical type must not be parsed more leniently just because it arrived through a different entry point. nym.AuditInfo additionally rejects a payload that carries none of the promoted crypto.AuditInfo fields: encoding/json leaves the embedded pointer nil in that case, and FromBytes returning success there would hand the caller a value whose promoted methods and fields nil-dereference. Both strictness gaps were closed for #2073.

crypto.AuditInfo.Validate guarantees only the four Attributes entries the default schema needs, while the schema string travels in the same payload and selects the positions schema.DefaultManager indexes — up to Attributes[27] for w3c-v0.0.1. EidNymAuditOpts and RhNymAuditOpts therefore bounds-check before indexing and return an error for an attribute-count/schema mismatch, rather than relying on callers to overwrite Schema with a trusted value first (which idemixnym.KeyManager.DeserializeAuditInfo does do today).

EidNymAuditData and RhNymAuditData embed mathlib curve elements, which JSON-encode as a curve ID plus the raw element bytes:

{"EidNymAuditData":{"Nym":{"curve":3,"element":"..."},"Rand":{...},"Attr":{...}}}

mathlib’s UnmarshalJSON uses that curve ID to index its internal curve table without a bounds check, so an out-of-range ID raises an index out of range panic from inside encoding/json. Both FromBytes implementations therefore run their decode through crypto.UnmarshalAuditInfo, which recovers that panic and returns it as an ordinary error:

return crypto.UnmarshalAuditInfo(func() error {
    return json.Unmarshal(raw, a)
})

The guard wraps the real decode rather than pre-validating the payload’s curve IDs, because mathlib runs during encoding/json’s traversal: a separate validation pass has to reproduce that traversal exactly to see every curve element the decode reaches, including ones that never appear in the decoded result (a duplicate key overwriting an earlier value, input after the first JSON value, a curve element following a type error). The same defect is contained the same way in FromG1Proto (token/core/zkatdlog/nogh/protos-go/utils/proto.go).

Where curve IDs arrive as plain data rather than through a third-party unmarshaler, prefer an explicit bounds check instead — see curveAt in token/core/common/encoding/asn1/asn1.go and PublicParams.Validate in token/core/zkatdlog/nogh/v1/setup/setup.go.

Other Identity Types

The architecture supports specialized identity types for complex use cases:

Multisig

Located in token/services/identity/multisig.

PolicyIdentity (Boolean-Expression-Governed Ownership)

Located in token/services/identity/boolpolicy.

HTLC (Hashed Time Lock Contract)

Located in token/services/identity/interop/htlc.

Extending the Identity Service

The Identity Service is designed to be extensible through the driver interfaces defined in Panurus. Custom identity implementations can be provided by implementing the required identity and wallet interfaces.

Typical extension scenarios include:

Step-by-Step Guide: Introducing a New Identity Type

The steps below describe how to add a new composite identity type end-to-end, based on the pattern used for PolicyIdentity (token/services/identity/boolpolicy).

Step 1 — Reserve a type tag

Add a new constant to token/driver/wallet.go alongside the existing tags:

const (
    // ...existing tags...
    MyNewIdentityType       IdentityType = 7
    MyNewIdentityTypeString              = "mynew"
)

The integer must be unique across all registered identity types.

Step 2 — Define the wire format

Create a package (e.g. token/services/identity/mynew/) and define the identity struct. Use ASN.1 DER for structured binary data (as PolicyIdentity does) or JSON for human-readable payloads (as HTLC does):

type MyNewIdentity struct {
    SomeField string `asn1:"utf8"`
    Parts     [][]byte
}

func (m *MyNewIdentity) Serialize() ([]byte, error) { return asn1.Marshal(*m) }
func (m *MyNewIdentity) Deserialize(raw []byte) error {
    // Never `_, err := asn1.Unmarshal(raw, m)`: discarding the "rest" return
    // accepts trailing garbage, which breaks the canonical encoding
    // requirement described under TypedIdentity above.
    return marshal.UnmarshalCanonicalDER(raw, m)
}

UnmarshalCanonicalDER separates the two ways a decode can fail. ErrNonCanonical, ErrTrailingBytes, ErrNonMinimalLen and ErrNonMinimalInt all mean the bytes are wrong and are the ones worth surfacing or counting as a rejected identity. ErrInvalidDestination means the destination pointer was nil — a caller bug, checked before the input is looked at, and never something a peer can provoke. Branch on the first group; treat the second as a programming error.

The destination is typed *T, not any, so passing a non-pointer or an untyped nil does not compile at all. Note also what ErrNonCanonical does and does not claim: the check is that the input is what asn1.Marshal emits for the destination type, which coincides with “canonical DER” only because the envelope types carry no optional, omitempty or time.Time fields. Do not point the helper at a type that does — the constraint is spelled out on the function and pinned by a test.

Expose Wrap / Unwrap helpers (see boolpolicy.WrapPolicyIdentity / boolpolicy.Unwrap) that embed the serialized struct inside a TypedIdentity envelope with the new type tag.

Step 3 — Implement signature verification

Add a Verifier that accepts the new signature format and a Deserializer that reconstructs a Verifier from raw identity bytes. Register the deserializer via des.AddTypedVerifierDeserializer(mynew.MyNewIdentityType, ...) in each driver’s NewTokenService (see token/core/fabtoken/v1/driver/driver.go and the zkatdlog equivalent).

Step 4 — Define the signature format

Define a struct for the signature produced over the token request (analogous to PolicySignature in boolpolicy/sig.go). Include ASN.1 or JSON encoding helpers and a JoinSignatures function if multiple parties contribute partial signatures.

Step 5 — Implement the Authorization checker

Create an EscrowAuth struct (see token/services/ttx/boolpolicy/auth.go) that implements the Authorization interface:

type EscrowAuth struct{ WalletService driver.WalletService }
func (a *EscrowAuth) AmIAnAuditor() bool                                  { return false }
func (a *EscrowAuth) IsMine(ctx context.Context, tok *token.Token) (string, []string, bool) { ... }
func (a *EscrowAuth) Issued(_ context.Context, _ driver.Identity, _ *token.Token) bool { return false }
func (a *EscrowAuth) OwnerType(raw []byte) (driver.IdentityType, []byte, error)        { ... }

Register it in common.NewStandardAuthorization (token/core/common/authorization.go), the chain both drivers use. Order matters: the multiplexer is first-match-wins and NewTMSAuthorization resolves any identity bound to a wallet — including composite identities — so it would claim the token under the wrong wallet id and drop its ownership entries. Every type-gated authorization must therefore be registered before the trailing NewTMSAuthorization catch-all (regression: TestAuthorizationOrder_BoundPolicyIdentity in token/services/ttx/boolpolicy/auth_test.go).

// token/core/common/authorization.go
func NewStandardAuthorization(logger logging.Logger, publicParameters driver.PublicParameters, walletService driver.WalletService) *AuthorizationMultiplexer {
	return NewAuthorizationMultiplexer(
		htlc.NewScriptAuth(walletService),
		multisig.NewEscrowAuth(walletService),
		boolpolicy.NewEscrowAuth(walletService),
		mynew.NewEscrowAuth(walletService), // ← add here, before the wallet-based catch-all
		NewTMSAuthorization(logger, publicParameters, walletService),
	)
}

Step 6 — Add a wallet wrapper

Create an OwnerWallet wrapper (see token/services/ttx/boolpolicy/wallet.go) that filters the unspent token list to tokens whose owner is the new identity type, and exposes domain-specific helpers (e.g. VerifyApprover).

Step 7 — Wire the recipient-negotiation protocol

If the new identity requires interactive negotiation between parties to assemble the composite identity before a transfer, add a RequestMyNewIdentity function following the pattern of ttx.RequestPolicyIdentity (token/services/ttx/recipients.go). The function sends a typed request, each counterparty responds with its component data, and the initiator assembles the final composite identity.

Step 8 — Add integration views

Create initiator and responder views in the integration layer (e.g. integration/token/fungible/views/mynew.go) following the pattern in boolpolicy.go:

Register all view factories and responders in the integration SDK (integration/token/fungible/sdk/party/sdk.go).

Step 9 — Add tests

Summary checklist

# What Where
1 Reserve type tag token/driver/wallet.go
2 Wire format + Wrap/Unwrap token/services/identity/mynew/
3 Verifier + Deserializer same package; register in both drivers
4 Signature format + JoinSignatures same package
5 EscrowAuth + register in drivers token/services/ttx/mynew/auth.go
6 OwnerWallet wrapper token/services/ttx/mynew/wallet.go
7 Recipient-negotiation protocol token/services/ttx/recipients.go
8 Integration views + SDK registration integration/token/fungible/views/mynew.go
9 Unit + integration tests alongside each new file