MonerochanNews

rss
Guides Opinions Interviews

Robust Monero Escrow with offline Arbitrators

During the development of the Serai network, a new piece of innovation was discovered. This piece brings us one step closer to the birth of a trade-based civilization, that does not rely on central authority. Now we can include arbitrators in an escrow wallet, without their active participation.

By Spirobel

The Serai decentralized exchange has to continue operations, even if some validators go offline:

With Serai, the point is to be robust and remain operational even if up to a third of validators go offline.

This means that the distributed key generation (DKG) protocol has to consider the case, where some validators are not present during the setup or rotation of the keys:

Our initial solution was to detect if a validator was offline, and if so, remove them from the key generation protocol. Unfortunately, this means they would be permanently excluded from the key and considered as ‘offline’ for its entire lifetime, even if they came back online later. While this was functional and acceptable, it clearly wasn’t optimal.

sources & further reading: serai frost dkg presentation slides audit blog post

Serai has solved this problem for multisig setups with up to 150 participants as part of their dex implementation.

What this means for the Escrow Use Case

Unfortunately, the current serai dkg crate is coupled to the work in progress and other crates in the serai workspace, some of them with custom patches applied. Especially if your project has the same rust dependencies, this can lead to a roadblock.

I wrote a new frost-dkg crate to make the serai dkg crate easier to work with. It also includes the FROST signing of Monero transactions. It has a JSON serialization-friendly API and is callable from C. The C ABI means it can also be used as a WASM module. There is typescript instrumentation for this module and it is integrated into the @spirobel/monero-wallet-api typescript package.

To set up a multisig wallet, t (threshold) of n (total multisig participants) need to be present. That is why we choose a 3 out of 5 multisig for the escrow case. Customer and Merchant each get two key shares and the arbitrator gets one. The merchant keeps one hot key on the shop server, so that the customer can reach the threshold of 3 and immediately set up the escrow wallet and pay.

In the dispute case, the one arbitrator key is still enough to break the tie, but merchant hot key compromise is not enough to empty the escrow wallet with just the arbitrator key.

With this library it is now possible to let arbitrators share their dkg public keys, similar to how people share their monero addresses and let them be included in the escrow wallets. The merchants define a list of arbitrators they are willing to accept. During the checkout process the customer chooses the arbitrator from this list. The customer wallet uses the arbitrator DKG public key to build an escrow wallet with the merchant hot key (plus the two customer dkg key pairs) during the checkout.

Acceptance Test Walkthrough

Now we go through the multisig escrow acceptance test, to see how the implementation and library API looks in detail.

The test starts by making key pairs for all participants to participate in the Distributed Key Generation (DKG) flow.

dkg_public_key_setup

In the serai network the view_key is known to all multisig participants. In the escrow case we create a shared secret between merchant and customer. This secret is then used as the private view key for the escrow wallet.

The test shows both merchant and customer arrive at the same viewkey:

escrow_view_key_ECDH

In the dispute case the customer can share the secret viewkey with the arbitrator to construct the transaction & then sign together to release the funds. In the normal flow the merchant will do that.

The DKG process itself has just 2 calls:

  1. participate()
  2. verify()

Let’s take a look at how the two customer dkg keys and the merchant hot key go successfully through the participate() step. And then use verify() to produce a valid escrow wallet Monero address.

The inputs are: the respective dkg secret key, all the public keys (customer, merchant, arbitrator) and the threshold number to unlock the funds (3 in our case). Theshold is also the number of necessary active participants to set up the wallet.

escrow_dkg_participate

The second verify() step of the DKG ceremony takes the participations, which is returned by the first participate() step and finally gives us the threshold keys that we can then use to sign Monero transactions.

escrow_dkg_verify

All verify() calls produce the same group key (which is the spend public key in Monero terms).

This spend public key together with the shared secret between customer & merchant from earlier, produces the escrow address.

At the end of test a, we have successfully set up the escrow wallet. The console logs show addresses for mainnet, stagenet and testnet. (regtest operates on mainnet addresses)

This test runs a regtest node in the background and fills a customer wallet with regtest coins in parallel to the DKG flow.

We can see at the beginning of test b, there is an output in the escrow wallet:

escrow_customer_paid

This is the result of the transaction that was sent from the customer wallet into the escrow wallet at the end of test a.

escrow_customer_tx

This simulates the typical checkout flow: the customer sets up the escrow wallet with the active participation of the merchant hot wallet & the passive inclusion of the arbitrator public dkg key.

Then the customer pays into the escrow wallet

Now at the beginning of test b, we see the merchant proposes a transaction to sweep all the funds from the escrow wallet into his merchant wallet.

This means that if this transaction is signed and sent, the funds that the customer paid for the goods into the escrow wallet, will be in the sole custody of the merchant.

Here we see the creation of the transaction & the loading of the threshold keys of the merchant + the arbitrator, so we reach the necessary threshold of 3 to sign the transaction:

escrow_sweep_to_merchant

Now to sign the transaction two calls are necessary:

  1. preprocess()
  2. sign()

All we need is the unsigned_tx, the threshold keys and we are done:

escrow_preprocess_sign

Now the merchant can combine the resulting signature shares and send the transaction:

escrow_complete_send

In the end we get the final outcome: The escrow wallet is empty and the merchant wallet contains the swept funds.

(very close to what the customer paid into the escrow wallet minus just tx fees)

escrow_final

You can run this test yourself by checking out this repo: https://github.com/monerochan-ecosystem/monero-wallet-api

  1. Follow the build instructions here https://github.com/monerochan-ecosystem/monero-wallet-api/tree/master/typescript
  2. Run the reorg handling test to place the regtest monero node: bun test tests/acceptance/reorg_handling.test.ts
  3. Run the escrow test bun test tests/acceptance/escrow.test.ts