This guide explains how to extend an existing token validator driver with custom validation functions.
This is useful when you need to enforce additional business rules or compliance checks beyond the default logic provided by the token drivers (e.g., FabToken or ZKAT-DLog).
Panurus uses a ValidatorDriverService to manage factories for creating driver.Validator instances.
Each driver version is identified by a unique string (e.g., zkatdlognogh.v1).
To extend a validator, you typically:
driver.ValidatorDriver that wraps an existing one.NewValidator method to inject additional validation logic.The ValidatorDriverService (found in token/core/service.go) maintains a map of driver identifiers to driver.ValidatorDriver implementations.
type ValidatorDriverService struct {
*factoryDirectory[driver.ValidatorDriver]
}
func (s *ValidatorDriverService) NewValidator(pp driver.PublicParameters) (driver.Validator, error) {
if driver, ok := s.factories[DriverIdentifierFromPP(pp)]; ok {
return driver.NewValidator(pp)
}
return nil, errors.Errorf("no validator found for token driver [%s]", DriverIdentifierFromPP(pp))
}
By providing a custom factory with the same identifier as an existing driver, you can effectively “hijack” the validator creation process.
Suppose you want to add a custom check to all transfer operations in a ZKAT-DLog system.
First, define a function that matches the signature expected by the validator. For ZKAT-DLog (NOGH v1), this is ValidateTransferFunc.
package myextension
import (
v1 "github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/setup"
"github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/token"
"github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/transfer"
"github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/validator"
"github.com/LFDT-Panurus/panurus/token/driver"
)
func MyCustomTransferValidation(ctx validator.Context, tr *transfer.Action) error {
// Perform your custom validation logic here.
// For example, check if the transfer metadata contains a specific attribute.
if len(tr.Metadata) == 0 {
return errors.New("transfer metadata is missing")
}
return nil
}
Implement the driver.ValidatorDriver interface by wrapping the standard one.
type MyValidatorDriver struct {
driver.ValidatorDriver // Wrap the existing driver
}
func (d *MyValidatorDriver) NewValidator(pp driver.PublicParameters) (driver.Validator, error) {
// We can't easily use the wrapped driver's NewValidator if we want to
// inject functions into its internal pipeline, so we replicate its logic.
ppp, ok := pp.(*v1.PublicParams)
if !ok {
return nil, errors.Errorf("invalid public parameters type [%T]", pp)
}
deserializer, err := driver.NewDeserializer(ppp) // Assume driver is the zkatdlog driver package
if err != nil {
return nil, err
}
logger := logging.DriverLoggerFromPP("token-sdk.driver.myextension", string(pp.TokenDriverName()))
// Instantiate the validator with your custom function
return validator.New(
logger,
ppp,
deserializer,
[]validator.ValidateTransferFunc{MyCustomTransferValidation}, // Extra transfer validators
nil, // Extra issuer validators
nil, // Extra auditor validators
), nil
}
Register your custom factory using Panurus’s registration mechanism. If you are using the dig container (standard in FSC-based applications), you can provide it to the token-validator-drivers group.
func NewMyValidatorDriver() core.NamedFactory[driver.ValidatorDriver] {
return core.NamedFactory[driver.ValidatorDriver]{
Name: core.DriverIdentifier(v1.DLogNoGHDriverName, v1.ProtocolV1),
Driver: &MyValidatorDriver{
// You might need to initialize the wrapped driver here
},
}
}
By using the same Name as the original driver, the ValidatorDriverService will use your factory instead of the default one.
If you want to add validation that is independent of the driver’s internal implementation, you can wrap the driver.Validator interface directly.
type WrappedValidator struct {
driver.Validator
}
func (v *WrappedValidator) VerifyTokenRequestFromRaw(ctx context.Context, getState driver.GetStateFnc, anchor driver.TokenRequestAnchor, raw []byte) ([]interface{}, driver.ValidationAttributes, error) {
// Call the original validator first
actions, attrs, err := v.Validator.VerifyTokenRequestFromRaw(ctx, getState, anchor, raw)
if err != nil {
return nil, nil, err
}
// Perform post-validation
for _, action := range actions {
if err := myGlobalCheck(action); err != nil {
return nil, nil, err
}
}
return actions, attrs, nil
}
This approach is highly portable and works across all token drivers.