Hamsa Privacy is a Layer 2 blockchain solution built on Microsoft's ZKP System that enables financial institutions to run private, EVM-compatible nodes for secure banking applications. The platform supports Central Bank Digital Currencies (CBDCs) and inter-bank transactions while maintaining data confidentiality through zero-knowledge proofs and regulatory compliance features.
Introduction
Hamsa Privacy is a Layer 2 solution built on Microsoft's ZKP System, designed to offer privacy, scalability, and compliance for financial institutions. By allowing institutions to operate dedicated and isolated nodes, it ensures that sensitive data remains private while supporting blockchain-based applications such as smart contracts and transaction rollups. Hamsa Privacy integrates with existing banking frameworks and provides tools to manage data confidentiality in regulated environments, making it suitable for use cases such as CBDCs (Central Bank Digital Currencies) and secure financial transactions.
Context
Hamsa Privacy, built on Microsoft's ZKP System, is a unique Layer 2 solution for the financial industry, designed to solve the following challenges:
- Support privacy for banking applications and data
- Support versatile banking applications on a blockchain-based platform
- Support SLA (Service Level Agreement) for transaction speed, throughput, and cost
Hamsa Privacy, built on Microsoft's ZKP System, is in beta phase. New use cases, requirements, pilot programs, and technology partners are welcome.
Architecture
The Hamsa Privacy Layer 2 architecture is shown in Figure 1..

Figura 1. Arquitetura da plataforma Hamsa Privacy
In Figure 1, each financial institution (e.g., bank) will have its own Layer 2 node. Who operates these nodes for financial institutions and who provides availability is not included in the scope of this document. Throughout this document, we will use the terms "bank" and "financial institution" interchangeably.
Hamsa Privacy is 100% EVM (Ethereum Virtual Machine) compatible. Client banks run their smart contracts on their own Hamsa Privacy nodes. Unlike other public Layer 2 networks, data stored in the ledger is not synchronized with other Hamsa Privacy nodes for privacy reasons.
Hamsa Privacy uses Microsoft (MSFT) Nova to perform transaction rollups and submit them to the Layer 1 network for verification. Critical rollup information (e.g., Merkle tree hash values of transactions and balances) is saved to the Layer 1 network for verification and tracking. Each Hamsa Privacy node has its own Layer 1 rollup verifier, but all share the same Dvp-match smart contract.
Hamsa Privacy Node Architecture
Node Diagram
The detailed diagram of a Hamsa Privacy node is shown in Figure 2..

Figura 2. Node architecture of Hamsa Privacy
We use the microservices pattern in the Hamsa Privacy implementation. Their responsibilities are listed below:
- Nodes: stores received transactions in the
tx_pool, manages transaction lifecycle, coordinates Dvp and Rollup flow - Executor: executes transactions using an EVM.
- Prover: generates rollup proof for transaction batches.
The ethTxManger component in Figure 2 is responsible for calling smart contracts on Layer 1. It will retry failed Layer 1 transactions if necessary.
The synchronizer component is responsible for fetching Layer 1 events and updating Layer 2 transaction status (primarily for Dvp implementation).
Access Control
For security, we implement a simple yet powerful authentication and authorization mechanism.
Each address can be assigned a role in the statedb.state.address_role table. By default, we define two roles:
- Role (id=0): normal user. Normal users can only query their own balances and transactions.
- Role (id=1): administrator user. Administrator users can query all balances and transactions on a Hamsa Privacy node.
It is not necessary to create a row in the address_role table for normal users. Regulators should have an address with role Id=1 on all Hamsa Privacy nodes in their jurisdiction.
We use Ethereum message signatures for authentication. While simple, our authorization method can be easily extended to provide more granular access control (e.g., implementing a granular access control system in a gateway before the Hamsa Privacy node).
Access control within smart contracts should be provided by the smart contract implementation. Hamsa is working to provide an Ethereum library.
Layer 1 Integration
A Hamsa Privacy node needs to call two smart contracts deployed on Layer 1:
- Dvp-match: only one instance of Dvp-match is deployed on Layer 1.
- Rollup-verifier: one instance is deployed for each Hamsa Privacy node.
Typically, Layer 1 networks are unstable. Many transactions can fail for various reasons; the ethTxManager component will perform retries.
To improve performance and decrease the chance of node conflicts, a list of Layer 1 Ethereum private keys should be configured for the ethTxManager to submit transactions to the Layer 1 network.
Rollup Architecture
Rollup Diagram

Diagram 3. Rollup Diagram
The purpose of the rollup is to verify that the account balance (and other information) is updated correctly according to the transactions in a batch. A batch can include multiple blocks. The rollup is executed per batch for ZKP circuit performance considerations. The information (i.e., public inputs and outputs of the circuit) stored in the Layer 1 verifier smart contract can be used for verification and tracking.
We use the Microsoft Nova circuit library for rollup proof generation and verification on Layer 1.
GitHub - microsoft/Nova, Nova: High-speed recursive arguments from folding schemes
Rollup DSL
Instead of implementing a zkEVM to rollup all transactions, Hamsa Privacy offers a lightweight solution. We define a set of rollup events, which define the purpose and effect of asset management smart contract functions. We perform rollups only for smart contracts that emit these events, for which we will maintain a Merkle tree of account balances in MongoDB. Any changes to the Merkle tree are sent to the Prover for proof generation and subsequently verified by a Layer 1 verification smart contract.
event HamsaTransferMade(address indexed from, address indexed to, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaApprovalMade(address indexed owner, address indexed spender, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaMintMade(address indexed account, address indexed minter, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaBurnMade(address indexed account, address indexed burner, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaDepositMade(address indexed depositor, address indexed account, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaWithdrawMade(address indexed withdrawer, address indexed account, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaFrozenMade(address indexed account, address indexed freezer, uint256 value, uint256 fromBalance, uint256 toBalance);
event HamsaUnfrozenMade(address indexed account, address indexed unfreezer, uint256 value, uint256 fromBalance, uint256 toBalance);
This list can be extended in the future as business requirements evolve.
Internally, we also use a similar method to prove fund movement in our dvp-escrow smart contract. We define the following events for an escrow-type smart contract:
event EscrowScheduleTransfer(address indexed from, address indexed to, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowScheduleBurn(address indexed burner, address indexed account, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowScheduleMint(address indexed account, address indexed minter, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowTransfer(address indexed from, address indexed to, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowBurn(address indexed burner, address indexed account, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowMint(address indexed account, address indexed minter, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowReturn(address indexed escrow, address indexed account, address tokenAddress, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowScheduleTransfer1155(address indexed from, address indexed to, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowScheduleBurn1155(address indexed burner, address indexed account, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowScheduleMint1155(address indexed account, address indexed minter, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowTransfer1155(address indexed from, address indexed to, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowBurn1155(address indexed burner, address indexed account, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowMint1155(address indexed account, address indexed minter, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
event EscrowReturn1155(address indexed escrow, address indexed account, address tokenAddress, uint256 tokenType, uint256 value, uint256 fromBalance, uint256 toBalance);
DvP Architecture
DvP Diagram

Diagrama 4. DvP Architecture
DvP is only necessary for inter-bank transactions. For transactions within the same bank, smart contracts on the same Layer 2 node can call each other. For inter-bank transactions (i.e., interbank transactions), the DvP implementation helps corroborate transactions across different Hamsa Privacy nodes.
In Diagram 4, when the executor detects that a "scheduleXXX" function of the dvp-escrow smart contract is executed, it will be included in a DvP job. At the end of transaction block execution, the DvP job information is saved to the PostgreSQL monitored_txs table. We provide 6 DvP-related functions in the dvp-escrow contract:
function scheduleBurn(ScheduleRequest request)
function scheduleMint(ScheduleRequest request)
function scheduleTransfer(ScheduleRequest request)
function scheduleTransfer1155(ScheduleRequest request)
function scheduleBurn1155(ScheduleRequest request)
function scheduleMint1155(ScheduleRequest request)
With the ScheduleRequest defined as:
struct ScheduleRequest {
address tokenAddress;
address to;
uint256 tokenType;
uint256 amount;
uint256 index;
uint256 chunkHash;
uint256 bundleHash;
uint256 expireTime;
}
The 6 functions above can be used in different combinations to implement business workflows.
For Drex phase 2 use cases, we anticipate the need to execute multiple token actions within a single DvP request, so we defined a new function:
function scheduleBurnMintAndGenerate(ScheduleRequest2 memory request) external returns (bool)
With the ScheduleRequest2 defined as:
struct ScheduleRequest2 {
uint256 index;
uint256 chunkHash;
uint256 bundleHash;
uint256 expireTime;
BurnRequest[] burnRequests;
MintRequest[] mintRequests;
BurnSettleRequest[] burnSettleRequests;
}
struct BurnRequest {
address tokenScAddress;
uint256 tokenType;
address account;
uint256 amount;
}
struct MintRequest {
address tokenScAddress;
uint256 tokenType;
address account;
uint256 amount;
}
struct BurnSettleRequest {
address tokenScAddress;
uint256 tokenType;
address account;
uint256 amount;
address toBankAddress;
}
The difference between BurnRequest and BurnSettleRequest is that the BurnRequest will trigger settlement in the L1-tmsc every 1 minute.
This new function will replace the other scheduleXXX functions. For phase 2 use cases, use only this new function. The usage of this function and its parameters are explained in detail in our development tutorial document.
In our current DvP implementation, each interbank transaction (i.e., a DvP bundle) can have two or three sub-transactions, identified by a chunk hash. The bundle hash and chunk hashes have the relationship:
bundle\_hash = poseidon\_hash(chunk\_hash1, chunk\_hash2, chunk\_hash3)
Within the dvp-match smart contract, we implement bundle verification and receipt verification.
In the DvP design, we intentionally separate the business transaction flow and the banking settlement flow, due to the fact that funds will be locked in the dvp-escrow smart contract during the DvP process. The purpose of this separation is to minimize the impact on banking settlement.
The steps for the wholesale TPFT purchase workflow are explained in Diagram 5.

Figura 5. Os passos para a compra de tokens TPFT no atacado
In the wholesale TPFT purchase workflow, banking settlement and TPFT token transfer always occur at the same time.
The steps for the retail TPFT purchase workflow are explained in Diagram 6.

Diagrama 6. Os passos para a compra de TPFT no varejo
The steps for the RCDBC token purchase settlement workflow are explained in Diagram 7.

Diagrama 7. Os passos para a liquidação de compra de tokens RCDBC
Supported APIs
The Hamsa Privacy team provides APIs to allow third-party systems to integrate with a Hamsa Privacy node. Ethereum message signatures are used for authentication. By design, we also implement only minimal role-based authorization on Hamsa Privacy nodes. It is the responsibility of the node business owner to implement a more granular access control policy (e.g., only KYC users can access your node, and only KYB nodes can participate in DvP transactions).
Authentication
We use Ethereum message signatures for authentication. The following sample code illustrates how to generate a message signature and use it for authentication in a Hardhat (JavaScript) script:
const \[deployer] = await ethers.getSigners();\
let signature = await deployer.signMessage("hello");
let address = await deployer.getAddress();
let balance = await ethers.provider.send("eth\_safeGetBalance", \[address, null, "hello", signature]);
Another example:
await ethers.provider.send("eth_getTransactionHistory", ["hello", signature]);
For normal users, this API will return their own transaction history per page. For administrator users, this API will return all users' transaction history per page.
In the MVP, only the following APIs are protected by authentication and authorization:
- eth_safeGetBalance
- eth_getTransactionHistory
eth_estimateGas
const transaction = {
to: ethers.ZeroAddress,
value: 0
};
const gasLimit = await ethers.provider.estimateGas(transaction);
console.log("gasLimit:", gasLimit.toString());
Note: when testing smart contracts using Hardhat, Hardhat will automatically call the estimateGas API.
eth_getNonce
const \[deployer] = await ethers.getSigners();\
let nonce = await deployer.getNonce();
Note: when testing smart contracts using Hardhat, Hardhat will automatically call the getNonce API.
Submit Transaction
If we call smart contract functions using a Hardhat script, Hardhat will call getNonce and estimateGas automatically. The following example shows how to deploy an ERC20 smart contract::
async function deploySimple() {
const [deployer] = await ethers.getSigners();
const SimpleToken = await ethers.getContractFactory("Simple");
const simple = await SimpleToken.deploy("simple", "$simple");
await simple.waitForDeployment();
console.log("simple is deployed at ", await simple.getAddress());
}
In some special cases (e.g., stress testing), we will need to create the transaction body and assign a nonce:
async function scheduleL2Transfer(escorting, uclWallet, nonce, chunkHash, bundleHash, expire) {
let schduleRequest = {
tokenAddress: simpleAddress,
to: '0x08883F8d938055aed23b0A64dcd7fD140028F648',
tokenType: 0,
amount: 100,
index: 0,
chunkHash: chunkHash,
bundleHash: bundleHash,
expireTime: expire
};
const data = await escorting.interface.encodeFunctionData("scheduleTransfer", [schduleRequest]);
return uclWallet.sendTransaction({
type: 0,
to: escortingAddress,
nonce: nonce,
value: 0,
gasLimit: 700000,
gasPrice: 1000,
data: data
});
}
For example, in stress testing, the gasLimit can be assigned to a large value. To manually assign the nonce in code, we can call getNonce to get the nonce before testing and then increment it by 1 per iteration.
eth_getTransactionByHash
After submitting a transaction, we can use the following code to get the transaction hash and receipt:
let tx = await simple.approve(escortingAddress, 100 * 1000000);
console.log("tx: ", tx.hash);
let receipt = await tx.wait();
To get transaction information later, we can use the following Ethereum RPC:
async function getTransactionProof() {
let info = await ethers.provider.send("eth_getTransactionByHash", ["0x0377c857f08bbd20b448eca3f54c933ba784c582bc6f5ea36a78c8f9e82c717c", false]);
console.log("tx info", info.transactionProof[0]);
}
The returned transaction proof and sender/recipient proofs can be verified in the Layer 1 verifier smart contract.
async function verifyProof() {
const L1Verifier = await verifier.attach("0x7056D9dc2B210769D4F96e1f36FbB278Be9A922F");
let smart_contract_address = "0x02f7aC504d940bb1f8C84724502745c787d6BaFa";
let balanceRoot = "0x95AF5372518C1C4A0D4B5C65EF25058B382FC166F792CE3EFF787BF6F628970";
let result = await verifier.queryBalanceRootExist(smart_contract_address, balanceRoot);
console.log("balance verification result: ", result);
let transactionRoot = "1108551082811432058010680812812024348758747321620579242231990872088052780371";
result = await verifier.queryTxRootExist(smart_contract_address, transactionRoot);
console.log("transaction verification result: ", result);
}
eth_getBalance
let balance = await ethers.provider.getBalance(address);\
console.log("balance: ", balance);
This function is not protected by authentication and authorization.
eth_checkTransactionBundle
Verifies DvP transaction information and status:
async function getBundleInfo() {
let bundleHash = "0x1f774b3c209088a4117a111ec005fc1cf9ab95b970a3e29c966addf77a83f7ee";
let BundleTransaction = await ethers.provider.send("eth_checkTransactionBundle", [bundleHash]);
console.log("bundle", BundleTransaction);
}
eth_blockNumber
Returns the last block number:
async function lastL2Block() {
let lastBlock = await ethers.provider.send("eth_blockNumber", []);
console.log("last Block: ", lastBlock);
}
eth_call
Allows instant execution of a new message call without requiring the creation of a transaction on the blockchain:
async function ethCallSample() {
const [deployer] = await ethers.getSigners();
const SimpleToken = await ethers.getContractFactory("Simple");
const simple = await SimpleToken.attach(simpleAddress);
const data = await simple.interface.encodeFunctionData("balanceOf", ["0x08883F8d938055aed23b0A64dcd7fD140028F648"]);
const params = {
to: simpleAddress,
data: data
};
let tx = await ethers.provider.send('eth_call', [params, 'latest']);
console.log("tx", tx);
}
eth_chainId
Returns the Layer 2 chain ID:
async function chaiId() {
let chaiId = await ethers.provider.send("eth_chainId", []);
console.log("chainId: ", chaiId);
}
eth_gasPrice
Returns the current base gas rate of the Layer 2 network:
async function gasPrice() {
let chaiId = await ethers.provider.send("eth_gasPrice", []);
console.log("chainId: ", chaiId);
}
eth_getBlockByHash
Retrieves information about a specific Layer 2 block with a specific block hash:
async function blockByHash() {
let blockHash = "0x89ac6ed7fda6be507454991e1faabfbb1ef7e88bc310802605fdeb8442b4c501";
let block = await ethers.provider.send("eth_getBlockByHash", [blockHash, false]);
console.log("block: ", block);
}
eth_getBlockByNumber
async function blockByNumber() {
let block = await ethers.provider.send("eth_getBlockByNumber", [8, false]);
console.log("block: ", block);
}
eth_getBlockTransactionCountByHash
Retrieves the transaction count of a block with a specific block hash:
async function transactionCountByHash() {
let blockHash = "0x579cb518a0528eda691d45edd21ff95e082aa8a6bafa2f7c77c93a389f73d8c4";
let block = await ethers.provider.send("eth_getBlockTransactionCountByHash", [blockHash]);
console.log("block: ", block);
}
eth_getBlockTransactionCountByNumber
Retrieves the transaction count of a block with a specific block number:
async function transactionCountByNumber() {
let block = await ethers.provider.send("eth_getBlockTransactionCountByNumber", [26]);
console.log("block: ", block);
}
eth_getTransactionReceipt
Retrieves the transaction receipt with a specific transaction hash:
async function printReceipt() {
const SimpleToken = await ethers.getContractFactory("Simple");
const simple = await SimpleToken.attach(simpleAddress);
let tx = await simple.mint("0x08883F8d938055aed23b0A64dcd7fD140028F648", 10);
let r = await tx.wait();
console.log("receipt: ", r);
}
eth_sendRawTransaction
Allows submitting a signed transaction to the network. After a transaction is signed, you can use the eth_sendRawTransaction method to submit the signed transaction to the Ethereum network for processing.
