Atomic cross-chain swap between NEM public and private chain

Cross-chain swaps enable trading tokens between different blockchains, without using an intermediary party in the process.

This exchange of tokens will succeed atomically. If some of the actors do not agree, each of them will receive the locked tokens back after a determined amount of time.

When talking about tokens in NEM, we are actually referring to mosaics. Catapult enables atomic swaps through SecretLock / SecretProof mechanism.

Background

Alice and Bob want to exchange 10 alice tokens for 10 bob tokens. The problem is that they are not in the same blockchain: alice token is defined in NEM public chain, whereas bob token is only present in a private chain using Catapult technology.

One non-atomic solution could be:

  1. Alice sends 10 alice tokens to Bob (private chain)
  2. Bob receives the transaction
  3. Bob sends 10 bob tokens to Alice (public chain)
  4. Alice receives the transaction

However, they do not trust each other that much. Bob could decide his mosaics to Alice. Following this guide, you will see how to make this swap possible, trusting technology.

Prerequisites

Getting into some code

Trading tokens directly from one blockchain to the other is not possible, due to the technological differences between them.

In case of NEM public and private chain, the same mosaic name could have a different definition and distribution, or even not exist. Between Bitcoin and NEM, the difference is even more evident, as each blockchain uses an entirely different technology.

Instead of transferring tokens between different chains, the trade will be performed inside each chain. The secret proof / secret lock mechanism guarantees the token swap occurs atomically.

sequenceDiagram participant Alice participant Private Chain participant Public Chain participant Bob Note over Alice: proof = 'random' Note over Alice: h = sha_256() Note over Alice: secret = h(proof) Alice ->> Private Chain: announces TX1(secret) Note right of Alice: TX1 waits for proof Alice -->> Bob: tells secret Bob ->> Public Chain: announces TX2(secret) Note left of Bob: TX2 waits for proof Alice ->> Public Chain: announces TX3(proof, secret) Note left of Public Chain: proof becomes public Note left of Bob: TX2 executes Note left of Bob: Alice receives funds Bob ->> Private Chain: announces TX4(proof, secret) Note right of Alice: TX1 executes Note right of Alice: Bob receives funds

Atomic cross-chain swap sequence diagram

For that reason, each actor involved should have at least one account in each blockchain.

const alicePublicChainAccount = Account.createFromPrivateKey('', NetworkType.MAIN_NET);
const alicePrivateChainAccount = Account.createFromPrivateKey('', NetworkType.MIJIN);

const bobPublicChainAccount = Account.createFromPrivateKey('', NetworkType.MAIN_NET);
const bobPrivateChainAccount = Account.createFromPrivateKey('', NetworkType.MIJIN);

const privateChainTransactionHttp = new TransactionHttp('http://localhost:3000');
const publicChainTransactionHttp = new TransactionHttp('http://localhost:3000');

const publicChainGenerationHash = process.env.NETWORK_GENERATION_HASH as string;
const privateChainGenerationHash = process.env.NETWORK_GENERATION_HASH as string;
  1. Alice picks a random number, called proof. Then, applies a SHA3-256 algorithm to it, obtaining the secret.
const random = crypto.randomBytes(10);
const proof = random.toString('hex');
console.log('Proof:', proof);
const hash = sha3_256.create();
const secret = hash.update(random).hex().toUpperCase();
console.log('Secret:', secret);
  1. Alice creates a SecretLockTransaction TX1, including:
  • Mosaic: 10 [520597229,83226871] alice token
  • Recipient: Bob’s address (Private Chain)
  • Algorithm: SHA3-256
  • Secret: SHA3-256(proof)
  • Duration: 96h
  • Network: Private Chain
const tx1 = SecretLockTransaction.create(
    Deadline.create(),
    new Mosaic(new MosaicId([520597229, 83226871]), UInt64.fromUint(10)),
    UInt64.fromUint(96 * 3600 / 15), // assuming one block every 15 seconds
    HashType.Op_Sha3_256,
    secret,
    bobPrivateChainAccount.address,
    NetworkType.MIJIN);

Once announced, this transaction will remain locked until someone discovers the proof that matches the secret. If after a determined period of time no one proved it, the locked funds will be returned to Alice.

  1. Alice signs and announces TX1 to the private chain.
const tx1Signed = alicePrivateChainAccount.sign(tx1, privateChainGenerationHash);
privateChainTransactionHttp
    .announce(tx1Signed)
    .subscribe(x => console.log(x), err => console.error(err));
  1. Alice can tell Bob the secret. Also, he could retrieve it directly from the chain.
  2. Bob creates a SecretLockTransaction TX2, which contains:
  • Mosaic: 10 [2061634929,1373884888] bob token
  • Recipient: Alice’s address (Public Chain)
  • Algorithm: SHA3-256
  • Secret: SHA3-256(proof)
  • Duration: 84h
  • Network: Public Chain
const tx2 = SecretLockTransaction.create(
    Deadline.create(),
    new Mosaic(new MosaicId([2061634929, 1373884888]), UInt64.fromUint(10)),
    UInt64.fromUint(84 * 3600 / 15), // assuming one block every 15 seconds
    HashType.Op_Sha3_256,
    secret,
    alicePublicChainAccount.address,
    NetworkType.MAIN_NET);

Note

The amount of time in which funds can be unlocked should be a smaller time frame than TX1’s. Alice knows the secret, so Bob must be sure he will have some time left after Alice releases the secret.

  1. Once signed, Bob announces TX2 to the public chain.
const tx2Signed = bobPublicChainAccount.sign(tx2, publicChainGenerationHash);
publicChainTransactionHttp
    .announce(tx2Signed)
    .subscribe(x => console.log(x), err => console.error(err));
  1. Alice can announce the SecretProofTransaction TX3 to the public network. This transaction defines the encrypting algorithm used, the original proof and the secret. It will unlock TX2 transaction.
const tx3 = SecretProofTransaction.create(
    Deadline.create(),
    HashType.Op_Sha3_256,
    secret,
    alicePublicChainAccount.address,
    proof,
    NetworkType.MAIN_NET);

const tx3Signed = alicePublicChainAccount.sign(tx3, publicChainGenerationHash);
publicChainTransactionHttp
    .announce(tx3Signed)
    .subscribe(x => console.log(x), err => console.error(err));
  1. The proof is revealed in the public chain. Bob picks the proof and announces the SecretProofTransaction TX4 to the private chain.
const tx4 = SecretProofTransaction.create(
    Deadline.create(),
    HashType.Op_Sha3_256,
    secret,
    bobPrivateChainAccount.address,
    proof,
    NetworkType.MIJIN);

const tx4Signed = bobPrivateChainAccount.sign(tx4, privateChainGenerationHash);
privateChainTransactionHttp
    .announce(tx4Signed)
    .subscribe(x => console.log(x), err => console.error(err));

Bob receives TX1 funds, and the atomic cross-chain swap concludes.

Is it really atomic?

Consider the following scenarios:

  • ✅ Bob does not want to announce TX2: Alice will receive her funds back after 94 hours.
  • ✅ Alice does not want to swap tokens by signing TX3: Bob will receive his refund after 84h. Alice will unlock her funds as well after 94 hours.
  • ⚠️ Alice signs and announces TX3, receiving Bob’s funds: Bob will have time to sign TX4, as TX1 validity is longer than TX2.

The process is atomic, but should be completed with lots of time before the deadlines.