Atomic cross-chain swap between public and private chain

Trade tokens between different blockchains without using an intermediary party in the process.

Use case

Alice and Bob want to exchange 10 alice.tokens for 10 bob.tokens. The problem is that they are not in the same network: alice.token is defined in a private chain using Symbol tech, whereas bob.token is only present in Symbol’s public chain.

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. As you may have noticed, Bob could keep Alice’s tokens if he opts not to perform 3.

This guide will show you how to define a secure way to exchange tokens between different participants and networks.

Prerequisites

  • Complete the getting started section.

  • All participant involved in the swap must own 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;
const alicePublicChainAccount = symbol_sdk_1.Account.createFromPrivateKey(
  '',
  symbol_sdk_1.NetworkType.MAIN_NET,
);
const alicePrivateChainAccount = symbol_sdk_1.Account.createFromPrivateKey(
  '',
  symbol_sdk_1.NetworkType.MIJIN,
);
const bobPublicChainAccount = symbol_sdk_1.Account.createFromPrivateKey(
  '',
  symbol_sdk_1.NetworkType.MAIN_NET,
);
const bobPrivateChainAccount = symbol_sdk_1.Account.createFromPrivateKey(
  '',
  symbol_sdk_1.NetworkType.MIJIN,
);
const privateChainTransactionHttp = new symbol_sdk_1.TransactionHttp(
  'http://localhost:3000',
);
const publicChainTransactionHttp = new symbol_sdk_1.TransactionHttp(
  'http://localhost:3000',
);
const publicChainGenerationHash = process.env.NETWORK_GENERATION_HASH;
const privateChainGenerationHash = process.env.NETWORK_GENERATION_HASH;
  • Alice account in the private chain must own at least 10 alice.tokens.

  • Bob account in the public chain must own at least 10 bob.tokens.

  • Both accounts should have enough network currency to pay for the transaction fees.

Method #01: Using the SDK

1. Alice generates a random set of bytes called proof. The proof should have a size between 10 and 1000 bytes. Then, applies a SHA3-256 algorithm to it, obtaining the secret.

  1. Alice hashes the obtained proof with one of the available algorithms to generate the secret.

const random = crypto.randomBytes(20);
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);
const random = crypto.randomBytes(20);
const proof = random.toString('hex');
console.log('Proof:', proof);
const hash = js_sha3_1.sha3_256.create();
const secret = hash.update(random).hex().toUpperCase();
console.log('Secret:', secret);
  1. Alice defines the SecretLockTransaction TX1:

TX1 Property

Value

Type

SecretLockTransaction

Mosaic

10 00D3378709746FC4 (alice token)

Recipient

Bob’s address (Private Chain)

Algorithm

SHA3-256

Duration

96 h

Secret

SHA3-256(proof)

Network

Private Chain

const tx1 = SecretLockTransaction.create(
  Deadline.create(epochAdjustment),
  new Mosaic(new MosaicId('00D3378709746FC4'), UInt64.fromUint(10)),
  UInt64.fromUint((96 * 3600) / 30), // assuming one block every 30 seconds
  LockHashAlgorithm.Op_Sha3_256,
  secret,
  bobPrivateChainAccount.address,
  NetworkType.MIJIN,
);
const tx1 = symbol_sdk_1.SecretLockTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  new symbol_sdk_1.Mosaic(
    new symbol_sdk_1.MosaicId('00D3378709746FC4'),
    symbol_sdk_1.UInt64.fromUint(10),
  ),
  symbol_sdk_1.UInt64.fromUint((96 * 3600) / 30), // assuming one block every 30 seconds
  symbol_sdk_1.LockHashAlgorithm.Op_Sha3_256,
  secret,
  bobPrivateChainAccount.address,
  symbol_sdk_1.NetworkType.MIJIN,
);

Once announced, this transaction will remain locked until someone discovers the proof that matches the secret. If no one unlocks it before the duration set is reached, the locked funds will be returned back to Alice.

  1. Alice announces TX1 to the private network and shares with Bob the secret.

Note

Bob should retrieve the secret from the chain. It is Bob’s responsibility to verify the secret correctness.

const tx1Signed = alicePrivateChainAccount.sign(
  tx1,
  privateChainGenerationHash,
);
privateChainTransactionHttp.announce(tx1Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
const tx1Signed = alicePrivateChainAccount.sign(
  tx1,
  privateChainGenerationHash,
);
privateChainTransactionHttp.announce(tx1Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
  1. Bob announces the following SecretLockTransaction TX2 to the public network

TX2 Property

Value

Type

SecretLockTransaction

Mosaic

10 10293DE77C684F71 (bob token)

Recipient

Alice’s address (Public Chain)

Algorithm

SHA3-256

Duration

84 h

Secret

SHA3-256(proof)

Network

Public Chain

const tx2 = SecretLockTransaction.create(
  Deadline.create(epochAdjustment),
  new Mosaic(new MosaicId('10293DE77C684F71'), UInt64.fromUint(10)),
  UInt64.fromUint((84 * 3600) / 30), // assuming one block every 30 seconds
  LockHashAlgorithm.Op_Sha3_256,
  secret,
  alicePublicChainAccount.address,
  NetworkType.MAIN_NET,
  UInt64.fromUint(2000000),
);
const tx2 = symbol_sdk_1.SecretLockTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  new symbol_sdk_1.Mosaic(
    new symbol_sdk_1.MosaicId('10293DE77C684F71'),
    symbol_sdk_1.UInt64.fromUint(10),
  ),
  symbol_sdk_1.UInt64.fromUint((84 * 3600) / 30), // assuming one block every 30 seconds
  symbol_sdk_1.LockHashAlgorithm.Op_Sha3_256,
  secret,
  alicePublicChainAccount.address,
  symbol_sdk_1.NetworkType.MAIN_NET,
  symbol_sdk_1.UInt64.fromUint(2000000),
);

Note

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

Note

To guarantee that TX1 cannot be rolled back, Bob must wait until TX1 receives at least maxRollBackBlocks confirmations before announcing TX2.

const tx2Signed = bobPublicChainAccount.sign(tx2, publicChainGenerationHash);
publicChainTransactionHttp.announce(tx2Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
const tx2Signed = bobPublicChainAccount.sign(tx2, publicChainGenerationHash);
publicChainTransactionHttp.announce(tx2Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
  1. Alice announces the SecretProofTransaction TX3 to the public network. This transaction defines the encrypting algorithm used, the original proof and the secret:

TX3 Property

Value

Type

SecretProofTransaction

Recipient

Alice’s address (Public Chain)

Algorithm

SHA3-256

Secret

SHA3-256(proof)

Proof

proof

Network

Public Chain

Note

To guarantee that TX2 cannot be rolled back, Alice must wait until TX2 receives at least maxRollBackBlocks confirmations before announcing TX3.

const tx3 = SecretProofTransaction.create(
  Deadline.create(epochAdjustment),
  LockHashAlgorithm.Op_Sha3_256,
  secret,
  alicePublicChainAccount.address,
  proof,
  NetworkType.MAIN_NET,
  UInt64.fromUint(2000000),
);

const tx3Signed = alicePublicChainAccount.sign(tx3, publicChainGenerationHash);
publicChainTransactionHttp.announce(tx3Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
const tx3 = symbol_sdk_1.SecretProofTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  symbol_sdk_1.LockHashAlgorithm.Op_Sha3_256,
  secret,
  alicePublicChainAccount.address,
  proof,
  symbol_sdk_1.NetworkType.MAIN_NET,
  symbol_sdk_1.UInt64.fromUint(2000000),
);
const tx3Signed = alicePublicChainAccount.sign(tx3, publicChainGenerationHash);
publicChainTransactionHttp.announce(tx3Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);
  1. Once TX3 is confirmed, the proof is revealed. TX2 transaction is unlocked, and Alice receives the locked funds.

  2. Bob picks the proof and announces the SecretProofTransaction TX4 to the private network, receiving the locked funds from TX1.

Note

To guarantee that TX3 cannot be rolled back, Bob must wait until TX3 receives at least maxRollBackBlocks before announcing TX4.

TX4 Property

Value

Type

SecretProofTransaction

Recipient

Bob’s address (Private Chain)

Algorithm

SHA3-256

Secret

SHA3-256(proof)

Proof

proof

Network

Private Chain

const tx4 = SecretProofTransaction.create(
  Deadline.create(epochAdjustment),
  LockHashAlgorithm.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),
);
const tx4 = symbol_sdk_1.SecretProofTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  symbol_sdk_1.LockHashAlgorithm.Op_Sha3_256,
  secret,
  bobPrivateChainAccount.address,
  proof,
  symbol_sdk_1.NetworkType.MIJIN,
);
const tx4Signed = bobPrivateChainAccount.sign(tx4, privateChainGenerationHash);
privateChainTransactionHttp.announce(tx4Signed).subscribe(
  (x) => console.log(x),
  (err) => console.error(err),
);

Is the process atomic?

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

  • ✅ Bob does not want to announce TX2: Alice will receive her funds back after 94 hours.

  • ✅ Alice does not announce TX3: Bob will receive his refund after 84h. Alice will unlock her funds as well after 94 hours.

  • ⚠️Alice signs and announces TX3: Alice receives Bob’s funds. Bob will have enough time to sign TX4 because TX1’s duration is longer than TX2’s.

  • ⚠️A rollback rewrites the history: Alice and Bob have waited at least maxRollBackBlocks between each transaction confirmation.