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:
- DApps to register
NotaryInfoconfigurations, each of which provisions a nativex/vcvverification route - Users to notarise assets by submitting a verifiable presentation that proves asset validity
- Native integration with
x/vcvfor 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:
- Build the content
routeIdand fetch the route viaVcvKeeper.GetRoute - Parse
asset_dataand calculate theAssetIdas a SHA256 hash of the asset components (x/notary/keeper/keeper_notarise.go,calculateAssetId) - Call
VcvKeeper.VerifyVerifiablePresentation, passing theverifier_inputas the verifiable presentation and the computedAssetId/OdpHashas thecompareTovalues the disclosed claims are checked against - If verification succeeds, store the
NotarisedAssetin 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:
- 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 - The
Notarisekeeper method builds the contentrouteIdand fetches the correspondingx/vcvroute - Asset data is parsed and the
AssetIdis calculated VcvKeeper.VerifyVerifiablePresentationis called with the route, the verifiable presentation (verifier_input), and the computedcompareTovalues (AssetId,OdpHash)x/vcvnatively verifies the SD-JWT presentation against the route's issuer requirements and verification materials- If verification passes, the asset is stored as a
NotarisedAssetand 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
- Separation of Concerns: x/notary handles business logic (NotaryInfo registration, asset storage, fees), while x/vcv handles all cryptographic verification.
- Native verification: Verification routes live in
x/vcvand are exercised through Go calls — verification runs entirely in-process with no contract addresses. - Deterministic route IDs: Each NotaryInfo maps to route IDs derived from the
MsgNotarisetype URL, a route marker, and the NotaryInfoId (e.g./d.notary.v1.MsgNotarise:content:1and/d.notary.v1.MsgNotarise:extop:1). - Dynamic comparison values: Runtime-computed values such as the
AssetIdare passed as thecompareTomap so verification can bind the presentation to the specific asset being notarised.