DLT Interoperability and More ⛓️#24— Privacy-Preserving Cross-Chain Atomic Swaps ; BONUS: Let’s Code⛓️

Rafael Belchior
10 min readMay 11


In this series, we analyze papers on blockchain and interoperability.

This edition covers a paper on a paper that proposes a protocol for rollups on different chains to communicate.

➡️ Title: Privacy-Preserving Cross-Chain Atomic Swaps
➡️ Authors: Apoorvaa Deshpande and Maurice Herlihy

➡️ Paper source: http://fc20.ifca.ai/wtsc/WTSC2020/WTSC20_paper_20.pdf

➡️ Background:

Our article introduces the area, types of blockchain interoperability, and how to choose a particular solution. You can read extensive background on interoperability from our paper published at ACM DLT.

➡️ Motivation:

Privacy in the cross-chain setting needs to be better studied. In our ongoing survey, we identified that a very few percentage of papers tackle this issue. This is one of them.

➡️ Contributions:

“− Formalizing Privacy in Cross-Chain Atomic Swaps. We define an atomic swap as a two-party protocol and formalize the different notions of privacy in the form of anonymity, confidentiality, and indistinguishability of swap transactions.

− Private Swaps from Atomic Release of Secrets. We abstract out the primitive of Atomic Release of Secrets (ARS) which captures atomic exchange of a secret for a pre-decided transaction. We then show how ARS can be used to build privacy-preserving cross-chain swaps.

− Instantiating Atomic Release of Secrets. We show that the recently introduced notion of adapter signatures [Poe18, War17] is a concrete instantiation of ARS under the framework of Schnorr signatures [Sch91]. This in turn enables a private cross-chain swap using Schnorr signatures.”

We will focus on the first contribution

💪 Strong points:

Under-explored problem. You can count with your fingers the numbers of papers working on cross-chain privacy (as of April 2023).

🤞 Suggestions for improvement:

As our future work, we are extending this model for general-purpose interoperability.

🔥 Points of interest:

The authors define a set of requirements for a private cross-chain asset exchange (e.g., HTLCs). The general idea is to use a cryptography to generate 2 different redeemable secrets such that only Alice and Bob know how to link them. We can do this with, for example, Diffie-Hellman. Diffie-Hellman is a protocol to exchange cryptographic keys.

So Alice and Bob can compute this shared secret that is used as an anchor to compute the secrets that allow redeeming assets in the cross-chain swap. The authors explain it very well (quoting from the paper):

Note that Bob could NOT redeem the tokens from Alice at any point because he does not know Sb — he helped generate Z via another local secret (). However, the timelocks prevent Bob from redeeming the assets before Alice. This is a vulnerability of the protocol, where Alice redeems the tokens first and attempts to perform either 1) a Denial of Service against the blockchain or 2) bring Bob’s connectivity down (e.g., bribing miners) such that Bob might not be able to redeem his tokens. On the other hand, Alice would not be vulnerable to the sore loser attack. The sore loser attack “rises when one party decides to halt participation partway through, leaving other parties’ assets locked up for a long duration”, namely Bob could halt participation. However, as Alice knows z, the swap would be redeemable by Bob, given timelock assumptions.

Here, the secret ingredient is a securely shared secret computed off-chain that links the transactions. If either party discloses this secret, an adversary can link the cross-chain transactions.


Z is computable only to Alice and Bob because it was calculated via Diffie-Hellman.

🚀 An implementation!

To showcase this protocol, I’ve implemented a privacy-preserving HTLC as a Hyperledger Cacti plugin, in Solidity (on-chain component) and Typescript (off-chain component). Note that this is not a production-ready implementation and should only be used for educational purposes.

The starting point is the excellent HTLC-Besu package created by Peter Somogyvari, a great friend and mentor, and Azahara Castaño. Let us follow the steps on the Remix Editor: https://remix.ethereum.org. First, feel free to fork my repo and checkout on the “private-htlc” branch. Alternatively, grab the code here.

Go to packages/cactus-plugin-htlc-eth-besu/src/main/solidity/contracts/HashTimeLock.sol. This is our starting point code:

So this code defines a simple state machine that keeps track of the state of a list of HTLCs (each starts at INIT/0, then goes to ACTIVE/1, and ends up at WITHDRAWN, EXPIRED, or REFUNDED). In practice, each deployment of this contract can manage many contracts.

We can see that for a contract to be withdrawn, we need to provide two inputs: a valid contract ID whose state is ACTIVE (ready to realize the trade), the timestamp has not to have expired, and we need to provide a valid secret such that the sha256 hash of the secret hashes to the hashlock.

Prior to the expiration of the contract, we can refund it, get its status, and other helper functions (that allow us to build off-chain clients).

Let us instantiate the contract to see its practicality. Compile the contract in Remix and then deploy it. Upon deployment, initialize a new contract (note you can obtain some of the arguments by running the following script: `npx run packages/cactus-plugin-htlc-eth-besu/src/main/typescript/private-htlc-helper.ts`):

  • outputAmount: 5000000000000000000 (5 Ether)
  • expiration: 1683003744 (find the current epoch via https://www.epochconverter.com/ and add 3600 for defining an expiration of 1 hour).
  • hashLock: “0x7f26beca0bf70019dc1460547cc905e9375aaf7eb33a0b043d91f1ed3e4c048e” ( Keccak-256 hash of “my_secret”); raw bytes32 of the secret is “0x6d795f7365637265740000000000000000000000000000000000000000000000”
  • receiver: “0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2” (second address in the Remix IDE)
  • outputNetwork: “goerli”
  • outputAddress: “0x5B38Da6a701c568545dCfcB03FcB875f56beddC4” (first address in the Remix IDE)

Do not forget to send 5 ether to the contract before calling “initialize”:

Deployed contract at: 0xd9145CCE52D386f254917e481eB44e9943F39138 (Remix VM). We should be good now to initialize an HTLC:

Note that this implementation is insecure: we should add access control to the contract such that only the receiver or a set of authorized parties (e.g., off-chain relayers) can trigger the withdraw (the withdraw will always go to the receiver). Or, on the other hand, we can leave the withdraw funcionality permissionless and incentivize a market for parties to do this for their users (getting a small fee from the contract for that — exercise to the reader, implement this functionality). Now, after you initialized the HTLC, note that you can obtain the contract ID via the transaction logs (index 3): 0xf6910c4591558b2134c697b6b5632fccbfd8cb77df304303f0eea7e2b1eac29a

If you try to refund the HTLC it won’t work because the expiration time has not passed (the assets are effectively locked, opening doors for the sore loser attack). Ok, so let’s simulate that another HTLC is deployed in another EVM-compatible blockchain, with the same cryptographic primitives that allow calculating the secret (namely keccak-256). Don’t forget to change the identity of the deployer and the ammount issued:

Deployed at 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95. Let’s now create another HTLC manager. The parameters will be similar:

  • outputAmount: 10000000000000000000 (10 Ether) — let’s say we are trading 5 tokens in blockchain A for 10 tokens in blockchain B.
  • expiration: 1683000144
  • hashLock: “0x7f26beca0bf70019dc1460547cc905e9375aaf7eb33a0b043d91f1ed3e4c048e”
  • receiver: “0x5B38Da6a701c568545dCfcB03FcB875f56beddC4” (don’t forget to change the receiver with the output)
  • outputNetwork: “ropsten”
  • outputAddress: “0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2”

We have:

Works. The contract ID given is 0xf923ae4dcd8708f4295aa4857e7f8af15a5a26aefce9950f1e0093d4a812bbe2, obtainable via the transaction logs:

Let’s now try to withdraw the HTLC on blockchain A.

0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 (now with 95 ether) will try to withdraw contract id 0xf6910c4591558b2134c697b6b5632fccbfd8cb77df304303f0eea7e2b1eac29a from the first deployed HTLC with secret 0x6d795f7365637265740000000000000000000000000000000000000000000000 (bytes32 of “my_secret”).

Upon hitting the “withdraw” transaction, we can observe that the receiver account (0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2) was credited 5 ether. The process is similar on the other deployed contract that simulates a different chain.

Now the cool thing about Cacti is that we can very easily launch a web service that consumes the interfaces of this contract, and allow us to automate processes that create, refund, and withdraw HTLCs (useful to build, for example, a cross-chain decentralized exchange based on HTLCs, typically called liquidity networks). That will probably be tackled in a future article. That code is here, btw: packages/cactus-plugin-htlc-eth-besu/src/main/typescript/plugin-htlc-eth-besu.ts. If you want to experiment with Cacti, follow the BUILD instructions. On the other hand, if you just want to play with the package, you can easily install it on your project via npm with ``yarn add @hyperledger/cactus-plugin-htlc-eth-besu``, and start using the provided API. In particular, you can use the “initialize” endpoint, which uses a Hyperledger Besu connector, and the compiled contract (bytecode), and the output ABI from the solidity compiler. Pretty cool, uh?

However, we say this solution does not provide cross-chain privacy because there is linkability. An observer can link both contracts in different chains and reach the conclusion that the hashLock is the same, in our case the string “0x7f26beca0bf70019dc1460547cc905e9375aaf7eb33a0b043d91f1ed3e4c048e” . Therefore, we can link the identities reciever and outputAddress, with msg.sender.

Private HTLCs

We now modify our smart contract to follow the algorithm presented at the beginning of this post:

Alice creates a HTLC on blockchain A and Bob does the same on blockchain B. Let us set parameters generator = 11; modulus = 109;

Step 0: Alice and Bob establish a private channel and calculate Z = g^(sa*b) by running Diffie Hellman. b is a random secret Bob generates just to calculate Z. It is not the secret Sb that yields Yb. Alice calculates 23 (Ya = 11^3 mod 109) and Bob defines B as 50. They interchange those values and run Diffie-Hellman. (g^b)^a mod 109 = (g^a)^b mod 109 = g^(a*b) mod 109 = 11^(3*50)mod109 = 43. Z = 43 is the shared secret kept private from the blockchain (if Z is leaked, privacy is compromised).

At this point we have the following state:

Alice generates a secret sa, Ya, and computes Z based on sa and b. Bob only knows that Z yields 43, but needs an efficient way to calculate sa, i.e., solving the discrete log problem. This would imply that he would have to calculate 11^(x*50)mod 109 = 43, for different values of x. For small values, brute force works, but for secrets long enough, this is impractical.

Step 1: secret from Alice = 3. Alice’s hashlock (Ya) = 11^Alice’s secret modulus 109, i.e., 11^3mod109 = 23

Alice’s HTLC deployment

  • outputAmount: 5000000000000000000 (5 Ether)
  • expiration: 1893515539 (2030, you have plenty of time to redeem it)
  • hashLock: “0x0000000000000000000000000000000000000000000000000000000000000017” ( Ya = 11¹³mod109)
  • receiver: “0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2” (second address in the Remix IDE)
  • outputNetwork: “goerli”
  • outputAddress: “0x5B38Da6a701c568545dCfcB03FcB875f56beddC4” (first address in the Remix IDE)
  • priv: [“11”,”109"]

You should obtain something similar:

Step 2: Bob’s hashlock (Yb) = 23*11^43 mod109 = 88.

Let us see Bob’s HTLC deployment:

  • outputAmount: 6000000000000000000 (6 Ether)
  • expiration: 2524624532 (2050, you have even more time to redeem it)
  • hashLock: “0x0000000000000000000000000000000000000000000000000000000000000058” (88) calculated at Step 3.
  • receiver: “0x5B38Da6a701c568545dCfcB03FcB875f56beddC4” (first address in the Remix IDE)
  • outputNetwork: “ropsten”
  • outputAddress: “0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2” (second address in the Remix IDE)
  • priv: [“11”,”109"]

Step 3: Alice computes Sb = Sa +Z = 3+ 43 = 46. The second secret has been constructed from pieces of both Alice and Bob’s secrets. Alice sends 46 to the smart contract on blockchain B and redeems the assets.


  • ID: HTLC id
  • Secret: “0x000000000000000000000000000000000000000000000000000000000000002E” (46)

How? The proof verification takes 46, does 11⁴⁶ mod 109 and checks it is equal to Yb, 88. Bob’s HTLC is redeemed, and Alice shows Sb.

The proof check is exponentiation:

In Remix (do not forget the contract id):

Step 4: Now Bob can do the same because he learned sb. He calculates Sa = 46–43 = 3, and sends 3 (0x0000000000000000000000000000000000000000000000000000000000000003)to the contract on blockchain A. The processSecret function takes 3 as the secret, exponentiates it, and verifies it is equal to Ya, i.e., 11³ mod 109 = 23. It works! Now, Bob can withdraw the HTLC Alice created, with the calculated secret sa = sb — Z = 46- 43 = 3.

Note: typo, as = sb -z = 3

“Since z is computable or known only to Alice and Bob, the values sA, sB are unlinkable to any other observer, thus the transactions on either of the chains are indistinguishable from regular transactions.” The hashlocks are different as well. Note that the contracts could be attempted to be de-anonymized by analyzing the generator and modulus arguments. The idea is to link HTLCs with the same modulus and generator. While those arguments could reveal a link between contracts, if one picks a standardized interval for those, it may become difficult to link a set of HTLCs sharing the same modulus and generator (public elements). There are a wide range of new cryptographic primitives being developed that allow to design similar systems.

Please make sure to check Hyperledger Cacti and the very cool variety of cross-chain interoperability plugins: https://github.com/hyperledger/cacti

A note of gratitude to André Augusto who reviewed this article and provided useful feedback.

🚀 What are the implications for our work?

  • As interoperability matures, developing privacy-friendly solutions will become a target for researchers and developers.



Rafael Belchior

R&D Engineer at Blockdaemon. Opinions and articles are my own and do not necessarily reflect the view of my employer. https://rafaelapb.github.io