Overview
The zkp2p protocol uses a registry system to manage whitelists, authorizations, and configuration for various protocol components. Registries decouple authorization logic from core contracts, enabling flexible upgrades and governance.
Registries allow the protocol to add new payment methods, escrows, orchestrators, and hooks without upgrading core contracts.
Registry Architecture
Core Registries
1. PaymentVerifierRegistry
Maps payment methods to their verifiers and supported currencies.
Location : contracts/registries/PaymentVerifierRegistry.sol
Data Structure
struct PaymentMethodConfig {
bool initialized;
address verifier;
mapping ( bytes32 => bool ) isCurrency;
bytes32 [] currencies;
}
mapping ( bytes32 => PaymentMethodConfig) public store;
bytes32 [] public paymentMethods;
Key Functions
Add Payment Method :
function addPaymentMethod (
bytes32 _paymentMethod ,
address _verifier ,
bytes32 [] calldata _currencies
) external onlyOwner
Example usage:
registry. addPaymentMethod (
keccak256 ( "venmo" ),
unifiedVerifierAddress,
[ keccak256 ( "USD" )]
);
Add Currencies :
function addCurrencies (
bytes32 _paymentMethod ,
bytes32 [] calldata _currencies
) public onlyOwner
Example:
registry. addCurrencies (
keccak256 ( "wise" ),
[
keccak256 ( "USD" ),
keccak256 ( "EUR" ),
keccak256 ( "GBP" )
]
);
Remove Payment Method :
function removePaymentMethod ( bytes32 _paymentMethod ) external onlyOwner
Removes the payment method and all associated currencies.
Query Functions
function isPaymentMethod ( bytes32 _paymentMethod ) external view returns ( bool );
function getPaymentMethods () external view returns ( bytes32 [] memory );
function getVerifier ( bytes32 _paymentMethod ) external view returns ( address );
function isCurrency ( bytes32 _paymentMethod , bytes32 _currencyCode ) external view returns ( bool );
function getCurrencies ( bytes32 _paymentMethod ) external view returns ( bytes32 [] memory );
Usage in Protocol
Orchestrator queries the registry during intent validation (from contracts/Orchestrator.sol:416):
address verifier = paymentVerifierRegistry. getVerifier (_intent.paymentMethod);
if (verifier == address ( 0 )) revert PaymentMethodDoesNotExist (_intent.paymentMethod);
Escrow validates payment methods during deposit creation (from contracts/Escrow.sol:1060):
if ( ! paymentVerifierRegistry. isPaymentMethod (paymentMethod)) {
revert PaymentMethodNotWhitelisted (paymentMethod);
}
2. EscrowRegistry
Whitelists Escrow contracts that can be used with the Orchestrator.
Location : contracts/registries/EscrowRegistry.sol
State Variables
bool public acceptAllEscrows;
mapping ( address => bool ) public isWhitelistedEscrow;
address [] public escrows;
Key Functions
Add Escrow :
function addEscrow ( address _escrow ) external onlyOwner
Remove Escrow :
function removeEscrow ( address _escrow ) external onlyOwner
Toggle Accept All :
function setAcceptAllEscrows ( bool _acceptAll ) external onlyOwner
When acceptAllEscrows == true, the Orchestrator accepts intents for any escrow (useful for permissionless deployments).
Query Functions
function isAcceptingAllEscrows () external view returns ( bool );
function getWhitelistedEscrows () external view returns ( address [] memory );
Usage in Protocol
Orchestrator validates escrow during intent signaling (from contracts/Orchestrator.sol:411):
if ( ! escrowRegistry. isWhitelistedEscrow (_intent.escrow) && ! escrowRegistry. isAcceptingAllEscrows ()) {
revert EscrowNotWhitelisted (_intent.escrow);
}
3. OrchestratorRegistry
Authorizes Orchestrator contracts that can call Escrow functions.
Location : contracts/registries/OrchestratorRegistry.sol
State Variables
mapping ( address => bool ) public override isOrchestrator;
Key Functions
Add Orchestrator :
function addOrchestrator ( address _orchestrator ) external override onlyOwner
Remove Orchestrator :
function removeOrchestrator ( address _orchestrator ) external override onlyOwner
Usage in Protocol
Escrow validates orchestrator for liquidity operations (from contracts/Escrow.sol:103):
modifier onlyOrchestrator () {
if ( msg.sender != address (orchestrator)) revert UnauthorizedCaller ( msg.sender , address (orchestrator));
_ ;
}
Payment Verifiers authorize orchestrators (from contracts/unifiedVerifier/BaseUnifiedPaymentVerifier.sol:48):
modifier onlyOrchestrator () {
require (orchestratorRegistry. isOrchestrator ( msg.sender ), "Only orchestrator can call" );
_ ;
}
The Escrow contract uses a direct reference to its orchestrator, while payment verifiers use the registry. This means Escrow trusts a single orchestrator, while verifiers can work with multiple.
4. NullifierRegistry
Prevents double-spending of off-chain payments.
Location : contracts/registries/NullifierRegistry.sol
State Variables
mapping ( bytes32 => bool ) public isNullified;
mapping ( address => bool ) public isWriter;
address [] public writers;
Access Control
Only addresses with write permissions (payment verifiers) can add nullifiers:
modifier onlyWriter () {
require (isWriter[ msg.sender ], "Only addresses with write permissions can call" );
_ ;
}
Key Functions
Add Nullifier (Writer Only):
function addNullifier ( bytes32 _nullifier ) external onlyWriter {
require ( ! isNullified[_nullifier], "Nullifier already exists" );
isNullified[_nullifier] = true ;
emit NullifierAdded (_nullifier, msg.sender );
}
Grant Write Permission (Owner Only):
function addWritePermission ( address _newWriter ) external onlyOwner
Revoke Write Permission (Owner Only):
function removeWritePermission ( address _removedWriter ) external onlyOwner
Query Functions
function isNullified ( bytes32 _nullifier ) external view returns ( bool );
function getWriters () external view returns ( address [] memory );
Usage in Protocol
UnifiedPaymentVerifier nullifies payments after verification (from contracts/unifiedVerifier/BaseUnifiedPaymentVerifier.sol:125):
function _validateAndAddNullifier ( bytes32 _nullifier ) internal {
require ( ! nullifierRegistry. isNullified (_nullifier), "Nullifier has already been used" );
nullifierRegistry. addNullifier (_nullifier);
}
5. PostIntentHookRegistry
Whitelists post-intent hooks that can be executed after fulfillment.
Location : contracts/registries/PostIntentHookRegistry.sol (interface reference)
Key Functions
function addHook ( address _hook ) external onlyOwner ;
function removeHook ( address _hook ) external onlyOwner ;
function isWhitelistedHook ( address _hook ) external view returns ( bool );
Usage in Protocol
Orchestrator validates hooks during intent signaling (from contracts/Orchestrator.sol:404):
if ( address (_intent.postIntentHook) != address ( 0 )) {
if ( ! postIntentHookRegistry. isWhitelistedHook ( address (_intent.postIntentHook))) {
revert PostIntentHookNotWhitelisted ( address (_intent.postIntentHook));
}
}
6. RelayerRegistry
Whitelists relayers that can signal multiple intents simultaneously.
Location : contracts/registries/RelayerRegistry.sol (interface reference)
Key Functions
function addRelayer ( address _relayer ) external onlyOwner ;
function removeRelayer ( address _relayer ) external onlyOwner ;
function isWhitelistedRelayer ( address _relayer ) external view returns ( bool );
Usage in Protocol
Orchestrator checks relayer status for multiple intent permission (from contracts/Orchestrator.sol:392):
bool canHaveMultipleIntents = relayerRegistry. isWhitelistedRelayer ( msg.sender ) || allowMultipleIntents;
if ( ! canHaveMultipleIntents && accountIntents[ msg.sender ].length > 0 ) {
revert AccountHasActiveIntent ( msg.sender , accountIntents[ msg.sender ][ 0 ]);
}
Whitelisted relayers can always have multiple active intents, even if allowMultipleIntents == false.
Payment Method Hashing
Payment methods and currencies are identified by their keccak256 hash:
// Payment method hashes
bytes32 venmoHash = keccak256 ( "venmo" ); // 0x8c5e...
bytes32 paypalHash = keccak256 ( "paypal" ); // 0x3a7f...
bytes32 wiseHash = keccak256 ( "wise" ); // 0x9d2e...
// Currency hashes
bytes32 usdHash = keccak256 ( "USD" ); // 0x783a...
bytes32 eurHash = keccak256 ( "EUR" ); // 0x4f3c...
bytes32 gbpHash = keccak256 ( "GBP" ); // 0x2e8d...
Convention : Payment methods and currencies should be lowercase when hashed for consistency.
Registry Upgrade Patterns
Pattern 1: Adding New Payment Method
// 1. Deploy new verifier (or use existing UnifiedPaymentVerifier)
UnifiedPaymentVerifier newVerifier = new UnifiedPaymentVerifier (...);
// 2. Add payment method to verifier
newVerifier. addPaymentMethod ( keccak256 ( "cashapp" ));
// 3. Register in PaymentVerifierRegistry
paymentVerifierRegistry. addPaymentMethod (
keccak256 ( "cashapp" ),
address (newVerifier),
[ keccak256 ( "USD" )]
);
// 4. Grant write permission to verifier on NullifierRegistry
nullifierRegistry. addWritePermission ( address (newVerifier));
Pattern 2: Migrating to New Orchestrator
// 1. Deploy new Orchestrator
Orchestrator newOrchestrator = new Orchestrator (...);
// 2. Add to OrchestratorRegistry
orchestratorRegistry. addOrchestrator ( address (newOrchestrator));
// 3. Update Escrow reference (governance)
escrow. setOrchestrator ( address (newOrchestrator));
// 4. Optionally remove old orchestrator
orchestratorRegistry. removeOrchestrator ( address (oldOrchestrator));
Pattern 3: Adding Currencies to Existing Method
// Add EUR and GBP support to Venmo
paymentVerifierRegistry. addCurrencies (
keccak256 ( "venmo" ),
[
keccak256 ( "EUR" ),
keccak256 ( "GBP" )
]
);
Registry Events
All registries emit events for off-chain indexing:
PaymentVerifierRegistry
event PaymentMethodAdded ( bytes32 indexed paymentMethod );
event PaymentMethodRemoved ( bytes32 indexed paymentMethod );
event CurrencyAdded ( bytes32 indexed paymentMethod , bytes32 indexed currencyCode );
event CurrencyRemoved ( bytes32 indexed paymentMethod , bytes32 indexed currencyCode );
EscrowRegistry
event EscrowAdded ( address indexed escrow );
event EscrowRemoved ( address indexed escrow );
event AcceptAllEscrowsUpdated ( bool acceptAll );
OrchestratorRegistry
event OrchestratorAdded ( address indexed orchestrator );
event OrchestratorRemoved ( address indexed orchestrator );
NullifierRegistry
event NullifierAdded ( bytes32 nullifier , address indexed writer );
event WriterAdded ( address writer );
event WriterRemoved ( address writer );
Access Control Summary
Registry Owner Actions Writer Actions Public Read PaymentVerifierRegistry Add/remove methods & currencies N/A Query methods, verifiers, currencies EscrowRegistry Add/remove escrows, toggle accept-all N/A Query whitelisted escrows OrchestratorRegistry Add/remove orchestrators N/A Check if address is orchestrator NullifierRegistry Add/remove writers Add nullifiers Check if nullifier used PostIntentHookRegistry Add/remove hooks N/A Check if hook whitelisted RelayerRegistry Add/remove relayers N/A Check if relayer whitelisted
Security Considerations
Owner Centralization All registries are owned by governance. Ensure multisig or DAO controls owner keys.
Writer Permissions Only grant NullifierRegistry write permissions to trusted payment verifiers.
Registry Hygiene Remove deprecated payment methods and verifiers to avoid confusion.
Accept-All Risk Enabling acceptAllEscrows allows untrusted escrows. Use with caution.
Query Patterns
Check if Payment Method Supported
bool isSupported = paymentVerifierRegistry. isPaymentMethod ( keccak256 ( "venmo" ));
if (isSupported) {
address verifier = paymentVerifierRegistry. getVerifier ( keccak256 ( "venmo" ));
bytes32 [] memory currencies = paymentVerifierRegistry. getCurrencies ( keccak256 ( "venmo" ));
}
Check if Currency Supported
bool supports = paymentVerifierRegistry. isCurrency (
keccak256 ( "wise" ),
keccak256 ( "EUR" )
);
Check if Escrow Whitelisted
bool isWhitelisted = escrowRegistry. isWhitelistedEscrow (escrowAddress);
bool acceptsAll = escrowRegistry. isAcceptingAllEscrows ();
bool canUse = isWhitelisted || acceptsAll;
Check if Payment Nullified
bytes32 nullifier = keccak256 ( abi . encodePacked (
keccak256 ( "venmo" ),
keccak256 (venmoTransactionId)
));
bool isUsed = nullifierRegistry. isNullified (nullifier);
Best Practices
Hash Consistency : Always use lowercase strings for payment method and currency hashes
Idempotent Queries : Design off-chain systems to tolerate registry changes
Event Monitoring : Index registry events to maintain off-chain state
Graceful Degradation : Handle removed payment methods/currencies gracefully in UI
Test Coverage : Test registry interactions in integration tests, not just unit tests
Escrow - Uses PaymentVerifierRegistry and Orchestrator reference
Orchestrator - Uses EscrowRegistry, PaymentVerifierRegistry, PostIntentHookRegistry, RelayerRegistry
Payment Verification - Uses OrchestratorRegistry and NullifierRegistry