前提
本記事における Atomic Swap は Hash Time Locked Contracts を用います。当方が新たに考えたロジックではなく、あくまでも既存の概念やサンプルソースを元にやってみた記事となります。以下 Hash Time Licked Contracts = HTLC と表記します。HTLC により本記事では以下チェーン間での Atomic Swap を実装しています
- Ethereum ⇄ Symbol
- Ethereum ERC20 ⇄ Symbol
- Ethereum ERC721 ⇄ Symbol
- Polygon ⇄ Symbol
- Polygon ERC20 ⇄ Symbol
- JPYC ERC20 ⇄ Symbol
Atomic Swap の検証として、EVM系のチェーン(EthereumやPolygon,JPYC) ⇄ EVM外のチェーン(Symbol)で取引を行います。
はじめに
Atomic Swap では、異なるブロックチェーン間でトークン同士を直接交換する事が出来ます。特定の取引所やDEXを経由する必要はなく、ユーザー同士のP2Pで取引を安全に完了する事が出来ます。今回、この Atomic Swap の手法として HTLC を用います。HTLC の説明は bitcoin wiki の定義を参照します。
A Hash Time Locked Contract or HTLC is a class of payments that use hashlocks and timelocks to require that the receiver of a payment either acknowledge receiving the payment prior to a deadline by generating cryptographic proof of payment or forfeit the ability to claim the payment, returning it to the payer.
The cryptographic proof of payment the receiver generates can then be used to trigger other actions in other payments, making HTLCs a powerful technique for producing conditional payments in Bitcoin.
訳
ハッシュタイムロック契約またはHTLCは、ハッシュロックとタイムロックを使用して、支払いの受信者が暗号化された支払い証明を生成することによって、期限前に支払いを受け取ったことを認めるか、支払いを請求する能力を放棄して、それを支払者に返すことを要求する支払いのクラスです。
実際にプログラムを書きながら理解していきたいと思います
なぜ必要か
私はブロックチェーンの将来は最強の1つのチェーンが存在するのではなく、凡ゆるチェーン同士が繋がって大きな経済圏を実現していると期待しています。各チェーンにはそれぞれ強みや特徴となる機能があったり、独自の文化やユーザー層がいたりします。それは Ethereum だったり Polygon だったり、 または mona、 Symbol だったり、例えば銀行や取引所発行のチェーンであったり、ステーブルコインであったりと。今回はその一例として HTLC でこれを実現します。
やり方の紹介
スクリプトは部分的に抜粋している為、ソースコードの全体は以下レポジトリを参照してください
HTLC の条件
今回紹介するHTLCを実行するには以下の条件を該当のチェーンが満たしている必要があります
- 両チェーンで同じハッシュ関数が使われている事
- 「一定時間経過するまでロックする」という条件をトランザクションに付加できる事
今回のソースコード上ではハッシュ関数は HASH256
、以下のような形式で発行しています。
計算式イメージ
proof = crypto.randomBytes(32)
hash256 = SHA256(SHA256(proof))
やり方1: EVM側でスマートコントラクトの実装
詳細なやり方は割愛します。以下のToshiさんの記事を参照下さい。
スマートコントラクトの実装自体は以下のレポジトリのものを流用しています。
具体的な手順を以下にリストアップしておきます
- HTLC の Solidity プロジェクトを自身のローカルにクローン
- Infra 等を用いてコントラクトのデプロイ先を用意
- Truffle 等を用いて Infra へコントラクトのデプロイ
- デプロイ成功時のログより Contract Address を取得
上記手順でチェーン上にスマートコントラクトがデプロイされたら以下のようにクライアント側の実装を行います
サンプルスクリプト
import { AbiItem } from 'web3-utils';
import HashedTimelockAbi from '../abis/HashedTimelock.json';
import { MintOptions } from '../models/core';
import { HTLCMintResult, HTLCWithDrawResult } from '../models/HTLC';
import { BaseHTLCService } from './BaseHTLCService';
/**
* HTLC operations on the Ethereum Test Net.
* Passing a value to the constructor will overwrite the specified value.
*/
export class HTLCService extends BaseHTLCService {
constructor(providerEndpoint: string, contractAddress: string) {
super(providerEndpoint, contractAddress, HashedTimelockAbi.abi as unknown as AbiItem);
}
/**
* Issue HTLC and obtain the key at the time of issue
*/
public async mint(
recipientAddress: string,
senderAddress: string,
secret: string,
amount: number,
options?: MintOptions
): Promise<HTLCMintResult> {
const value = this.web3.utils.toWei(this.web3.utils.toBN(amount), 'finney');
const lockPeriod = Math.floor(Date.now() / 1000) + (options?.lockSeconds ?? 3600);
const gas = options?.gasLimit ?? 1000000;
return await this.contract.methods
.newContract(recipientAddress, secret, lockPeriod)
.send({ from: senderAddress, gas: gas.toString(), value });
}
/**
* Receive tokens stored under the key at the time of HTLC generation
*/
public async withDraw(contractId: string, senderAddress: string, proof: string, gasLimit?: number) {
const gas = gasLimit ?? 1000000;
const result = await this.contract.methods
.withdraw(contractId, proof)
.send({ from: senderAddress, gas: gas.toString() });
return { result: result as HTLCWithDrawResult };
}
}
各メソッドの意味
- mint = ロックされたトランザクションの発行
- withDraw = ロックされたトランザクションのロック解除
- refund = もしロック期間を経過してしまった場合に資産を引き戻す
やり方2: Symbol 側でコントラクトロジックの実装
EVM系と異なり、Symbolには自分でスマートコントラクトをゼロからデプロイするという概念がありません。事前に検証されたスマートコントラクトがビルトインされており、これをレゴブロックのように組み合わせて安全なコントラクトを実行します。Symbol には以下名称のコントラクトが用意されており、これをAPI経由で利用する事でHTLCを実現できます
- Secret lock transaction
- Secret proof transaction
サンプルスクリプト
import crypto from 'crypto';
import { firstValueFrom } from 'rxjs';
import { NetworkType } from 'symbol-sdk/dist/src/model/network/NetworkType';
import { RepositoryFactoryHttp } from 'symbol-sdk/dist/src/infrastructure/RepositoryFactoryHttp';
import { Deadline } from 'symbol-sdk/dist/src/model/transaction/Deadline';
import { Account } from 'symbol-sdk/dist/src/model/account/Account';
import { SecretLockTransaction } from 'symbol-sdk/dist/src/model/transaction/SecretLockTransaction';
import { Mosaic } from 'symbol-sdk/dist/src/model/mosaic/Mosaic';
import { MosaicId } from 'symbol-sdk/dist/src/model/mosaic/MosaicId';
import { UInt64 } from 'symbol-sdk/dist/src/model/UInt64';
import { LockHashAlgorithm } from 'symbol-sdk/dist/src/model/lock/LockHashAlgorithm';
import { Address } from 'symbol-sdk/dist/src/model/account/Address';
import { Convert } from 'symbol-sdk/dist/src/core/format/Convert';
import { SecretLockInfo } from 'symbol-sdk/dist/src/model/lock/SecretLockInfo';
import { SecretProofTransaction } from 'symbol-sdk/dist/src/model/transaction/SecretProofTransaction';
import { sha3_256 as sha3 } from 'js-sha3';
import { RawAddress } from 'symbol-sdk/dist/src/core/format/RawAddress';
import { Transaction } from 'symbol-sdk/dist/src/model/transaction/Transaction';
import { SignedTransaction } from 'symbol-sdk/dist/src/model/transaction/SignedTransaction';
import { MintOptions } from '../models/core';
import { HashPair } from '../models/core';
/**
* Handles secret locks and secret proofs in Symbol
* Create a key with `createHashPair` --> issue a lock transaction with `mint` --> unlock with `withDraw`.
*/
class HTLCSymbolService {
private readonly node: string;
private readonly networkType: NetworkType;
private readonly generationHashSeed: string;
private readonly epochAdjustment: number;
constructor(node: string, networkType: NetworkType, generationHashSeed: string, epochAdjustment: number) {
this.node = node;
this.networkType = networkType;
this.generationHashSeed = generationHashSeed;
this.epochAdjustment = epochAdjustment;
}
/**
* create a new hash pair
* If you specify an existing secret or proof in the constructor, take over that value
*/
public createHashPair(): HashPair {
const s = crypto.randomBytes(32);
const p1 = crypto.createHash('sha256').update(s).digest();
const p2 = crypto.createHash('sha256').update(p1).digest();
return {
proof: s.toString('hex').toUpperCase(),
secret: p2.toString('hex').toUpperCase(),
};
}
/**
* Issue a secret lock and return the results and key.
*/
public mint(
recipientAddress: string,
mosaicId: string,
secret: string,
amount: number,
options?: MintOptions
): SecretLockTransaction {
return SecretLockTransaction.create(
Deadline.create(this.epochAdjustment),
new Mosaic(new MosaicId(mosaicId), UInt64.fromUint(amount)),
UInt64.fromUint(options?.lockSeconds ?? 5760),
LockHashAlgorithm.Op_Hash_256,
secret,
Address.createFromRawAddress(recipientAddress),
this.networkType
).setMaxFee(options?.gasLimit ?? 100) as SecretLockTransaction;
}
/**
* Issue a transaction to receive a token using a pre-shared key
*/
public withDraw(recipientAddress: string, proof: string, secret: string) {
return SecretProofTransaction.create(
Deadline.create(this.epochAdjustment),
LockHashAlgorithm.Op_Hash_256,
secret,
Address.createFromRawAddress(recipientAddress),
proof,
this.networkType
).setMaxFee(100) as SecretProofTransaction;
}
/**
* sign & accounce
*/
public async sign(senderPrivateKey: string, tx: Transaction): Promise<SignedTransaction> {
const senderAccount = Account.createFromPrivateKey(senderPrivateKey, this.networkType);
const signedTransaction = senderAccount.sign(tx, this.generationHashSeed);
const txRepo = new RepositoryFactoryHttp(this.node).createTransactionRepository();
await firstValueFrom(txRepo.announce(signedTransaction));
return signedTransaction;
}
}
export default HTLCSymbolService;
各メソッドの意味
- mint = ロックされたトランザクションの発行
- withDraw = ロックされたトランザクションのロック解除
- sign = 作成したトランザクションをチェーン上へ公開する
該当トランザクションのより詳細な利用方法は以下のガイドブックも参照下さい
やり方3: HTLC を各チェーンで実行する
HTLC での取引は以下シーケンス図で示すような手順で取引を行います。
ポイントは以下図のように、取引の起点となるユーザー(ここでは Alice )しか、チェーン上にロックされているトランザクションの鍵を知らないという事です。Alice が先だしで Bob に対してトランザクションを発行し、Bobはそれを見て取引内容が正しければ Alice と同じ鍵を持つトランザクションを発行します。この時点では Bob は鍵を知りません。Alice は Bob のトランザクションを見て取引内容が正しければ、所有している鍵を用いて Bob のトランザクションへ署名します。この署名で鍵を含むトランザクションがパブリック空間に公開される為、Alice の持っていた鍵をBobは知る事が出来ます。そして Bob はその鍵を使って Alice のトランザクションのロックを解除し、トークンを受け取る事が出来ます。
一言メモ
※ ロックされたトランザクションは鍵であるProofがチェーン上に公開されるか、一定時間が経過するまで維持される
※ 利用するハッシュ関数が同一である為、同じSecretを利用すればProofは同一であると判断できる
ソースコードでも見て行きましょう
サンプルスクリプト(Symbol)
import { Transaction, RepositoryFactoryHttp, Account, NetworkType, Address } from 'symbol-sdk';
import { Contracts } from '../src/models/Contracts';
import HTLCSymbolService from '../src/servicies/HTLCSymbolService';
import { SYMBOL } from './config';
(async () => {
// setup
const client = new HTLCSymbolService(
Contracts.symbol.testnet.endpoint,
NetworkType.TEST_NET,
Contracts.symbol.testnet.generationHashSeed,
Contracts.symbol.testnet.epochAdjustment
);
const recipientAccount = Account.createFromPrivateKey(SYMBOL.PRIVATEKEY.TO, NetworkType.TEST_NET);
const senderAccount = Account.createFromPrivateKey(SYMBOL.PRIVATEKEY.FROM, NetworkType.TEST_NET);
const hashPair = client.createHashPair();
// mint
const transaction = client.mint(recipientAccount.address.plain(), SYMBOL.CURRENCY.MOSAIC_ID, hashPair.secret, 1);
const signedTx = await client.sign(SYMBOL.PRIVATEKEY.FROM, transaction);
console.log('----- wait until transaction is approved -----', {
fromAddress: senderAccount.address.pretty(),
toAddress: recipientAccount.address.pretty(),
transactionHash: signedTx.hash,
proof: hashPair.proof,
secret: hashPair.secret,
});
// Wait for secret transaction to be approved
await waitConfirmedTransaction(senderAccount.address, signedTx.hash);
setTimeout(async () => {
const drawTx = client.withDraw(recipientAccount.address.plain(), hashPair.proof, hashPair.secret);
const signedTx = await client.sign(recipientAccount.privateKey, drawTx);
console.log('waiting...', signedTx.hash);
await waitConfirmedTransaction(recipientAccount.address, signedTx.hash);
console.log(signedTx);
}, 3000);
})();
サンプルスクリプト(eth-erc20)
import { ETH } from './config';
import { HTLCERC20Service } from '../src/servicies/HTLCERC20Service';
import { Contracts } from '../src/models/Contracts';
(async () => {
// setup
const { PRIVATEKEY, TOKEN } = ETH;
const client = new HTLCERC20Service(Contracts.sepolia.erc20.endpoint, Contracts.sepolia.erc20.contractAddress);
const AccountService = client.web3.eth.accounts;
const fromAddress = AccountService.wallet.add(PRIVATEKEY.FROM).address;
const toAddress = AccountService.wallet.add(PRIVATEKEY.TO).address;
const hashPair = client.createHashPair();
// mint
const result = await client.mint(toAddress, fromAddress, hashPair.secret, 1, TOKEN.ALICE);
console.log('----- Lock transaction enlistment completed -----', {
fromAddress: fromAddress,
toAddress: toAddress,
contractId: result.events.HTLCERC20New.returnValues.contractId,
transactionHash: result.transactionHash,
proof: hashPair.proof,
secret: hashPair.secret,
contractInfo: await client.getContractInfo(result.events.HTLCERC20New.returnValues.contractId),
});
// issue
const res = await client.withDraw(result.events.HTLCERC20New.returnValues.contractId, toAddress, hashPair.proof);
console.log('----- Start withDraws -----');
console.log('withDraw', `https://sepolia.etherscan.io/tx/${res.result.transactionHash}`);
console.log(await client.getContractInfo(res.result.events.HTLCERC20Withdraw.returnValues.contractId));
})();
上記スクリプトを実行すると各チェーン上で最終的に取引が実行された事がわかると思います。本来であれば、相手のトランザクションを見て検証して、といったユーザー側のアクションが必要になりますが、上記は例としてまとめて実行しています。
こういった形式でトランザクションを発行する事で異なるチェーン間でもオンチェーンで取引を実行する事が出来ます。取引所やDEXを介さずともP2Pにて取引を完結する事が出来ます。もしくは自身が開発したサービス上でチェーン間の取引を行う事も可能です。凡ゆるチェーンが連携し、より高度な経済圏を構築していく未来に向けて是非皆様もやってみて貰えたら嬉しいです。
今後本レポジトリのコードは npm へも publish していきます。
最後に
実際に HTLC で取引を実行する際には以下の点には注意しましょう。人の手でアナログで完遂する事は手間でありミスの可能性もある為、このロジックを HTLC を用いるアプリへ丁寧に実装する事をお勧め致します。
- 例では Bob は Alice より Secret を受け取って共通の鍵を持つロックトランザクションを発行しますが、Bob はこの Secret を全面的に信用してはなりません。 Alice に悪意がある場合、この Secret を誤ったもので生成したり、もしくは作成したトランザクション内の取引内容が Bob にとって正しくない内容で作成されている可能性があります。必ず Bob は Secret 元のトランザクションを検証し、安全であることを確認する必要があります。Bob がこれをスクリプトを用いて機械的に行う事は問題ありません。
- Alice が発行したトランザクションのLock期間よりBob のトランザクションのLock期間は短い必要があります。例えば Bob がロックトランザクションを発行した後に、Alice が自身のトランザクションのロック期間が経過するのを待ってトランザクションが無効になった後に、Bobのロックトランザクションを鍵を用いて解除してしまうと、Bobは自身のトランザクションの中身はAliceに取得されてしまうものの、Aliceの発行したトランザクションの中身を Bob は受け取る事が出来ません。
- 各シークレットロックトランザクションは、厳密には各チェーン上でのファイナライズを待つべきです。もし、チェーンでロールバックが発生するとどちらかの、もしくは両方のロックトランザクションが無効になってしまう場合があります。よって、本 HTLC を用いたトランザクションの実行は完了まで時間を要します。Alice のロックトランザクションのファイナライズまで待機 → Bob のロックトランザクションのファイナライズまで待機 → Aliceがロック解除 → Bob がロック解除 と進む必要があります。