はじめに
HTLCとは、Hash Time-Locked Contractsである。これは、秘密の値(パスワード的な)もしくは、所定の時間が経てばロックが解除され、所有権を得ることができる仕組みである。
これを使って、中間者を経由したトラストレスな取引ができる。オリジナルの素晴らしい記事はこちら。
Understanding the Lightning Network, Part 2: Creating the Network
https://bitcoinmagazine.com/articles/understanding-the-lightning-network-part-creating-the-network-1465326903/
これをカタパルトでやってみたい。
シナリオ
- AliceはCarolに1XEMを支払う
- 支払いは、Bobを通して行う
- 本当はそれぞれ信頼関係にない
- 誰かが持ち逃げできないような仕組みにする
STEP BY STEP
まず、Carolは秘密の値xc0
を生成します。そのハッシュ値H(xc0)
をAliceへ送ります。
Aliceは、Carolからもらったハッシュ値を使って、Bobへの送金をするSecret Lockトランザクションを送信します。
同様に、BobはCarolへの送金をするSecret Lockトランザクションを送信します。
Carolは、このSecret Lockを解除するためのxc0
を持っているので、これを使ってSecret Proofトランザクションを送信します。
すると、チェーン上でxc0
が見れるようになるため、BobもSecret Proofトランザクションを送信し、お金を受け取ります。
まとめる
まとめるとこのようになります。
ここで重要なのが、「2」のトランザクションに対して、「3-1」のトランザクションの方が有効期限が短くてはならないということです。
なぜなら、「3-1」のロック解除が完了した時点で、「2」がのロック解除ができない事態が発生し、Bobが資金を失ってしまうからです。
そのためには、有効期限の指定が、ブロックに取り込まれてからのブロック数、ではなく、ブロック番号そのものを指定できるようなものである必要があります。
しかし、Secret Lockトランザクションの有効期限は、ブロックに取り込まれてからのブロック数を指定するような仕様になっています。
例えばここでは、「2」の有効期限を15000ブロック、「3-1」のを10000ブロックとしていますが、「2」がブロックに取り込まれてから5000ブロック以上経った後に、「3-1」を送信するようなことができてしまう可能性があります。
そのため、ビットコインでは、OP_CSVではなく、OP_CLTVが使われています。
対策1
その対策のひとつ目が、以下です。
ふたつのSecret Lockトランザクションを、アグリゲートトランザクションにして、同じブロックに必ず取り込まれるようにするものです。
追記:しかし、この2つを同じトランザクションにしてしまったら、3者間HTLCの意味がない気がします。
対策2
Deadlineを使い、有効期限のブロック数を同数にすることで、必ず「3-1」が先に有効期限を迎えるようにします。
追記:3-1のトランザクションのDeadlineを強制させることは難しいです。なので、この方法は対策になってませんでした。
対策3
対策1と似てますが、2つのSecret LockトランザクションをどちらもAliceが送信しようという点では同じです。
2-1をAggregate Completeにして、2-2のAggregate BondedのためのLockFundsを含めておきます。2-2がブロックに入るのは、2-1がブロックに入ってから2000ブロック以内になりますので、3000ブロック以上の期限差を作ることができます。
追記:BobとCarolの取引にAliceが絡んでしまうと、3者間HTLCの意味がない気がします。
旧課題
カタパルトのマイルストーンCまでは、同じハッシュ値を使って2つのSecret Lockトランザクションを送信することができませんでした。2つ目を送信すると、Failure_Lock_Hash_Exists
というエラーになります。
マイルストーンDからは可能になりました。ただし、ハッシュ値と受け取り人アドレスの組み合わせで一意でなければなりません。
やってみる
- カタパルトマイルストーンF
- nem2-sdk 1.13.4
Alice, Bob, Carolの各アカウントは、毎回新規作成します。そして、残高のあるアカウントからそれらへ送金します。
前項で記述した対策はせずに、素直にトランザクションの送信のみ行います。
const {
Account,
AccountHttp,
Deadline,
HashType,
Listener,
NetworkType,
NetworkHttp,
Mosaic,
MosaicId,
NamespaceId,
PlainMessage,
SecretLockTransaction,
SecretProofTransaction,
TransactionHttp,
TransferTransaction,
UInt64
} = require('nem2-sdk');
const op = require('rxjs/operators');
const rx = require('rxjs');
const crypto = require("crypto");
const { sha3_256 } = require('js-sha3');
process.env.HOST = 'http://fushicho.48gh23s.xyz:3000';
process.env.GENERATION_HASH = '9A7949B3ED05DE9C771B8BEB16226E1CEBCA4C50428F27445796C8B4D9B0A9D6';
const alice = Account.generateNewAccount(NetworkType.MIJIN_TEST)
const bob = Account.generateNewAccount(NetworkType.MIJIN_TEST)
const carol = Account.generateNewAccount(NetworkType.MIJIN_TEST)
const rich = Account.createFromPrivateKey(
'25B3F54217340F7061D02676C4B928ADB4395EB70A2A52D2A11E2F4AE011B03E',
NetworkType.MIJIN_TEST
)
const exec = async () => {
console.log("Alice", alice.address.plain(), "Bob", bob.address.plain(), "Carol", carol.address.plain())
const transactionHttp = new TransactionHttp(process.env.HOST);
const networkHttp = new NetworkHttp(process.env.HOST);
const accountHttp = new AccountHttp(process.env.HOST, networkHttp);
const distTxHashs = [alice, bob, carol].map((account) => {
const transferTransaction = TransferTransaction.create(
Deadline.create(),
account.address,
[new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(10000000))],
new PlainMessage(''),
NetworkType.MIJIN_TEST,
UInt64.fromUint(20000)
);
const signedTransaction = rich.sign(
transferTransaction,
process.env.GENERATION_HASH
);
transactionHttp.announce(signedTransaction);
console.log(signedTransaction.hash);
return signedTransaction.hash;
});
for (let i = 0; i < 1000; i++) {
const txs = await accountHttp.outgoingTransactions(rich.address).toPromise()
const distTxsConfirmed = txs.filter((tx) => {
const index = distTxHashs.findIndex((a) => {
return a === tx.transactionInfo.hash
})
return index > -1
})
if (distTxsConfirmed.length === 3) {
break;
}
console.log("waiting distribution transaction confirmed");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
// 1. Carol create cx0, then tell hcx0 to Alice
const random = crypto.randomBytes(10);
const cx0 = random.toString('hex').toUpperCase();
const hasher = sha3_256.create();
const hcx0 = hasher.update(random).hex().toUpperCase();
console.log("1. Carol", "\n", cx0, "\n", hcx0)
// 2. Alice lock to Bob
const tx2 = SecretLockTransaction.create(
Deadline.create(),
new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(1000000)),
UInt64.fromUint(15000),
HashType.Op_Sha3_256,
hcx0,
bob.address,
NetworkType.MIJIN_TEST,
UInt64.fromUint(100000)
);
const tx2Signed = alice.sign(tx2, process.env.GENERATION_HASH)
transactionHttp.announce(tx2Signed)
console.log("2. Alice", tx2Signed.hash)
for (let i = 0; i < 1000; i++) {
const txs = await accountHttp.outgoingTransactions(alice.address).toPromise()
if (txs.findIndex((x) => x.transactionInfo.hash === tx2Signed.hash) > -1) {
break;
}
console.log("waiting Alice transaction confirmed");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
// 3-1. Bob lock to Carol
const tx31 = SecretLockTransaction.create(
Deadline.create(),
new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(1000000)),
UInt64.fromUint(10000),
HashType.Op_Sha3_256,
hcx0,
carol.address,
NetworkType.MIJIN_TEST,
UInt64.fromUint(100000)
);
const tx31Signed = bob.sign(tx31, process.env.GENERATION_HASH)
transactionHttp.announce(tx31Signed)
console.log("3-1. Bob", tx31Signed.hash)
for (let i = 0; i < 1000; i++) {
const txs = await accountHttp.outgoingTransactions(bob.address).toPromise()
if (txs.findIndex((x) => x.transactionInfo.hash === tx31Signed.hash) > -1) {
break;
}
console.log("waiting Bob transaction confirmed");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
// 3-2. Carol unlock
const tx32 = SecretProofTransaction.create(
Deadline.create(),
HashType.Op_Sha3_256,
hcx0,
carol.address,
cx0,
NetworkType.MIJIN_TEST,
UInt64.fromUint(100000)
);
const tx32Signed = carol.sign(tx32, process.env.GENERATION_HASH)
transactionHttp.announce(tx32Signed)
console.log("3-2. Carol", tx32Signed.hash)
for (let i = 0; i < 1000; i++) {
const txs = await accountHttp.outgoingTransactions(carol.address).toPromise()
if (txs.findIndex((x) => x.transactionInfo.hash === tx32Signed.hash) > -1) {
break;
}
console.log("waiting Carol transaction confirmed");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
// 4. Bob unlock
const tx4 = SecretProofTransaction.create(
Deadline.create(),
HashType.Op_Sha3_256,
hcx0,
bob.address,
cx0,
NetworkType.MIJIN_TEST,
UInt64.fromUint(100000)
);
const tx4Signed = bob.sign(tx4, process.env.GENERATION_HASH)
transactionHttp.announce(tx4Signed)
console.log("4. Bob", tx4Signed.hash)
for (let i = 0; i < 1000; i++) {
const txs = await accountHttp.outgoingTransactions(bob.address).toPromise()
if (txs.findIndex((x) => x.transactionInfo.hash === tx4Signed.hash) > -1) {
break;
}
console.log("waiting Bob transaction confirmed");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
console.log("done")
}
exec();
実行結果
Alice SCPOPUEU5YZBT7ZUPFMAXLIHXBPITXEP2UMTKLND
Bob SCLHPAD3TRR665GKRPCHTY3HQNNNFAZQNT4B5Q4R
Carol SAPJMP7G5ZS2MN2QO2RWCJXUF6PK3G337LTNNROW
322AAD2AFDD6EF3D7681EDE9BEE662E48D7C01CC0FC99B84EB8187B64A006D8A
4DC6FDB5DAE0E3D1B178037EA7F7338EED699C1448D66E2AFF1EE74C401769C1
F95D7B4AA329FA46D8BCC6CBC3370A13601AA21B217710ED8D63BF6E7A4A64FE
...
waiting distribution transaction confirmed
1. Carol
B5E9AF9DFF582C0810AF
5EC848CABCC953CC3655767786673727521FA804A50A8BEBC25AAE8B0818CCDB
2. Alice F045044CA1E2F96D266CF1CA345BCE8829FDA68109E1BF15C809F0600CCCE5CE
waiting Alice transaction confirmed
...
3-1. Bob 1E43B836413E4BC6FF764222CE3B51633542317E79AA3F9631740421A9B98F98
waiting Bob transaction confirmed
...
3-2. Carol E23BFA9EC5AEB26D14BA8118937D1DADCB373E96488870BA97F7947F85C5667F
waiting Carol transaction confirmed
...
4. Bob 464C0A249AD053683BA8BEED92BCBA03C5C723F4EE3C19427275FAB5EB87B536
waiting Bob transaction confirmed
...
waiting Bob transaction confirmed
done
おわりに
どの対策が有用なのか、この時点ではよくわかりません。
これにペイメントチャネルを組み合わせると、ライトニングネットワークになるみたいです。勉強します。