Skip to main content

Concept

This module facilitates decentralised applications to register notarisation configurations for assets and to bring verifiable asset data onchain. The module integrates with the native x/vcv (Verifiable Credential Verification) module, which performs all cryptographic verification of verifiable presentations (SD-JWT) natively in Go.

For more background design information, see the Verifiable data notary section.

Overview

The x/notary module enables:

  1. DApps to register NotaryInfo configurations, each of which provisions a native x/vcv verification route
  2. Users to notarise assets by submitting a verifiable presentation that proves asset validity
  3. Native integration with x/vcv for all cryptographic verification (SD-JWT), without any smart contracts

The sequence of events is as follows:

How It Works

When a DApp registers a NotaryInfo, the notary module provisions up to two native x/vcv routes for it: an optional extension-option route (enforced by the vcv ante handler against a verifiable presentation carried as a tx extension option) and a mandatory content route (enforced by the Notarise keeper method against the verifier_input in the message body). When a user later notarises an asset under that NotaryInfo, the notary module looks the content route up and asks x/vcv to verify the supplied verifiable presentation against it:

MsgRegisterNotaryInfo → SetNotaryInfoMap → compileRegisterRoute → VcvKeeper.SetRoute (ext-opt route, if provided; content route)

MsgNotarise (ante) → GetMsgExtensionOptionRequirements → VcvKeeper.VerifyVerifiablePresentation (VP from extension option)
MsgNotarise (handler) → VcvKeeper.GetRoute (content) → Calculate AssetId → VcvKeeper.VerifyVerifiablePresentation → Store Asset → Burn fee

How x/notary Uses x/vcv

The x/notary module depends on the x/vcv module through the VCVKeeper interface (x/notary/types/expected_keepers.go):

type VCVKeeper interface {
SetRoute(ctx sdk.Context, routeId string, route vcvtypes.Route) error
GetRoute(ctx sdk.Context, routeId string) (vcvtypes.Route, error)
RemoveRoute(ctx sdk.Context, routeId string) error
VerifyVerifiablePresentation(
ctx sdk.Context,
route vcvtypes.Route,
vp vcvtypes.VerifiablePresentation,
compareTo map[string]interface{},
) error
}

The VCVKeeper is injected into the notary keeper during initialization (x/notary/keeper/keeper.go).

1. SetRoute (during NotaryInfo registration)

When a DApp registers a NotaryInfo, the notary keeper compiles the flat RegisterNotaryRoute params from the msg into native x/vcv routes and stores them (x/notary/keeper/verifier.go, x/notary/keeper/ms_notaryinfo.go). The issuers map is constructed in state — it cannot be carried in the tx because gogoproto map entries aren't resolvable by the tx decoder's unknown-field scan:

route := &vcvtypes.Route{
Admin: admin, // the NotaryInfo admin
Typ: r.Typ, // currently only SD-JWT (ROUTE_TYPE_SD_JWT)
Requirements: r.Routes, // claim requirements (attribute + criterion)
Issuers: map[string]*vcvtypes.Issuer{r.Issuer: {VerificationMaterials: r.VerificationMaterials}},
}

Two routes can be provisioned per NotaryInfo: the content route (mandatory, verified by the Notarise keeper method) and the extension-option route (optional, verified by the vcv ante handler). Their route IDs are deterministically derived from the MsgNotarise type URL, a route marker, and the NotaryInfoId (x/notary/keeper/verifier.go):

func (k Keeper) BuildNotarisationExtOptRouteId(notaryInfoId uint64) string {
notaryInfoIdStr := strconv.FormatUint(notaryInfoId, 10)
return strings.Join([]string{sdk.MsgTypeURL(&types.MsgNotarise{}), vcvtypes.ExtensionOptionRouteMarker, notaryInfoIdStr}, vcvtypes.RouteIdDivider)
}

func (k Keeper) BuildNotarisationContentRouteId(notaryInfoId uint64) string {
notaryInfoIdStr := strconv.FormatUint(notaryInfoId, 10)
return strings.Join([]string{sdk.MsgTypeURL(&types.MsgNotarise{}), vcvtypes.ContentRouteMarker, notaryInfoIdStr}, vcvtypes.RouteIdDivider)
}

With RouteIdDivider == ":", ExtensionOptionRouteMarker == "extop" and ContentRouteMarker == "content", this yields route IDs such as /d.notary.v1.MsgNotarise:extop:1 and /d.notary.v1.MsgNotarise:content:1. The routes are owned and managed natively inside x/vcv — no contract address is stored or referenced.

2. GetRoute + VerifyVerifiablePresentation (during notarisation)

When a user submits MsgNotarise, the notary keeper retrieves the content route and asks x/vcv to verify the supplied verifiable presentation against it (x/notary/keeper/keeper_notarise.go):

routeId := k.BuildNotarisationContentRouteId(notaryInfoId)
route, err := k.VcvKeeper.GetRoute(sdkCtx, routeId)
// ... parse asset data, compute AssetId ...
err = k.VcvKeeper.VerifyVerifiablePresentation(
sdkCtx,
route,
vcvtypes.VerifiablePresentation{Presentation: verifierInput},
map[string]interface{}{
"AssetId": assetId,
"OdpHash": invoiceData.OdpHash,
},
)

The notarisation flow:

  1. Build the content routeId and fetch the route via VcvKeeper.GetRoute
  2. Parse asset_data and calculate the AssetId as a SHA256 hash of the asset components (x/notary/keeper/keeper_notarise.go, calculateAssetId)
  3. Call VcvKeeper.VerifyVerifiablePresentation, passing the verifier_input as the verifiable presentation and the computed AssetId/OdpHash as the compareTo values the disclosed claims are checked against
  4. If verification succeeds, store the NotarisedAsset in state and burn the notarisation fee

3. VPVerifierI implementation (ante handler integration)

The notary keeper implements the x/vcv VPVerifierI interface (x/notary/keeper/exported.go) so the vcv ante handler (x/vcv/ante/ante.go, VcvExtensionOptionsDecorator) can recognise notary messages that carry verifiable-presentation extension options:

func (k Keeper) GetMsgExtensionOptionRequirements(ctx sdk.Context, msg sdk.Msg) (*vcvtypes.Route, map[string]interface{}, error) {
switch m := msg.(type) {
case *notarytypes.MsgNotarise:
routeId := k.BuildNotarisationExtOptRouteId(m.NotaryInfoId)
route, err := k.VcvKeeper.GetRoute(ctx, routeId)
// If route id is not found, it means the user did not provide any extension option routes while registering the NotaryInfo
if err != nil {
if errors.Is(err, collections.ErrNotFound) {
return nil, nil, nil
}
return nil, nil, err
}
return &route, map[string]interface{}{"notary_info_id": m.NotaryInfoId}, nil
default:
return nil, nil, nil
}
}

For a MsgNotarise, the keeper looks up the extension-option route for the message's NotaryInfoId. If none was registered, it returns a nil route and the ante handler skips VP verification for that message — the ext-opt route is optional. If a route exists, the ante handler extracts the VerifiablePresentation from the tx's extension options and calls VcvKeeper.VerifyVerifiablePresentation with the returned route and compareTo map ({"notary_info_id": ...}). The notary keeper is registered alongside the vcv keeper in the decorator's []exported.VPVerifierI list (app/ante.go).

Route Configuration

A notarisation route is a native x/vcv Route (proto/d/vcv/v1/verifier.proto):

message Route {
string admin = 1;
RouteType typ = 2; // currently only ROUTE_TYPE_SD_JWT
repeated Requirement requirements = 3;
map<string, Issuer> issuers = 4;
}

message Issuer {
repeated VerificationMaterial verification_materials = 1; // JWK public keys
}

message Requirement {
string attribute = 1;
Criterion criterion = 2;
}

Each Route carries its Requirements (an attribute plus a Criterion) and references one or more issuers, each with VerificationMaterials (JWK public keys used to verify the presentation signature). Whether a route's requirements apply to the message content or to a tx extension option is determined by the route ID marker (content vs extop), not by the route structure itself. The notary module itself does not implement any verification logic — it provisions the routes and delegates verification to x/vcv.

Verification Flow

When a user submits a MsgNotarise transaction:

  1. If an extension-option route exists for the NotaryInfoId, the vcv ante handler verifies the verifiable presentation carried as a tx extension option before the message is executed
  2. The Notarise keeper method builds the content routeId and fetches the corresponding x/vcv route
  3. Asset data is parsed and the AssetId is calculated
  4. VcvKeeper.VerifyVerifiablePresentation is called with the route, the verifiable presentation (verifier_input), and the computed compareTo values (AssetId, OdpHash)
  5. x/vcv natively verifies the SD-JWT presentation against the route's issuer requirements and verification materials
  6. If verification passes, the asset is stored as a NotarisedAsset and the notarisation fee is burned

Native SD-JWT Verification

x/vcv performs SD-JWT (Selective Disclosure JWT) verification natively in Go. SD-JWT enables privacy-preserving credential verification where the holder selectively discloses only the required claims from their verifiable credential. Verification material (JWK public keys) and the claim requirements are configured on the route's issuers; no external smart contract is consulted.

Key Design Points

  1. Separation of Concerns: x/notary handles business logic (NotaryInfo registration, asset storage, fees), while x/vcv handles all cryptographic verification.
  2. Native verification: Verification routes live in x/vcv and are exercised through Go calls — verification runs entirely in-process with no contract addresses.
  3. Deterministic route IDs: Each NotaryInfo maps to route IDs derived from the MsgNotarise type URL, a route marker, and the NotaryInfoId (e.g. /d.notary.v1.MsgNotarise:content:1 and /d.notary.v1.MsgNotarise:extop:1).
  4. Dynamic comparison values: Runtime-computed values such as the AssetId are passed as the compareTo map so verification can bind the presentation to the specific asset being notarised.