Skip to main content

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

RegistryOwner ActionsWriter ActionsPublic Read
PaymentVerifierRegistryAdd/remove methods & currenciesN/AQuery methods, verifiers, currencies
EscrowRegistryAdd/remove escrows, toggle accept-allN/AQuery whitelisted escrows
OrchestratorRegistryAdd/remove orchestratorsN/ACheck if address is orchestrator
NullifierRegistryAdd/remove writersAdd nullifiersCheck if nullifier used
PostIntentHookRegistryAdd/remove hooksN/ACheck if hook whitelisted
RelayerRegistryAdd/remove relayersN/ACheck 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

  1. Hash Consistency: Always use lowercase strings for payment method and currency hashes
  2. Idempotent Queries: Design off-chain systems to tolerate registry changes
  3. Event Monitoring: Index registry events to maintain off-chain state
  4. Graceful Degradation: Handle removed payment methods/currencies gracefully in UI
  5. 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

Build docs developers (and LLMs) love