前の記事では、ハッシュ タイム ロック コントラクト (HTLC) を使用したクロスチェーン アトミックスワップの実装について紹介しました。今日は、HTLC を使用せずに実現できる代替方法を紹介します。これにより、アトミック スワップがハッシュ ロックやタイム ロックのないブロックチェーンに拡張されます。
SPV を使用してトランザクションがマイニングされたことを証明する
合意された価格で、アリスの BTC コインをボブの BSV コインに交換しましょう。
この記事では、BSV スマート コントラクトがブロックチェーンに含まれるトランザクションを検証できることを示しました。重要なのは、これは外部のオラクルに依存せずに達成されることです。基本的なアイデアは、プルーフ・オブ・ワークの難易度を使用してブロック・ヘッダーを検証することです。マークル証明を使用すると、オフチェーンSPVで行われるのと同様に、トランザクションがブロックに含まれていることを検証できます。
画像:BSV アカデミー
これは、BSV 上のスマート コントラクトを、BTC などのブロックチェーン上の特定のトランザクションの確認に基づいて特定のアクションを実行するようにプログラムできることを意味します。
アトミック スワップのコンテキストでは、スマート コントラクトは基本的に次のように述べています。
「X BTC を Y アドレスに送金すると、その契約で現在保持されている BSV 資産を取得できます。」
この機能は、BSV コントラクトが BTC ブロックチェーン上で特定のトランザクションが正常にマイニングされたかどうかを検証できるため可能になります。コントラクトは特定の BTC トランザクションを検証し、予想される量の BTC が正しいアドレスに送信されたことを確認できます。
オファーを開始するために、ボブはスマート コントラクトで BSV コインを確保し、BTC を受け取るために必要なアドレスを指定します。アリスが BTC を送信すると、コントラクトにロックされている BSV を要求できるようになります。このプロセスには信頼できる仲介者は関与しません。
完全なプロトコルシーケンス
BSV と BTC 間のアトミック スワップ プロトコルは、以下の手順で実行できます。
-
コントラクトの展開: ボブは、BSV チェーンにスマート コントラクトを展開します。この契約は、ボブがアリスと交換する予定の BSV 資金を保持します。この契約には、フェイルセーフのタイムロックも含まれています。
-
支払いと証明: アリスは、合意された数の BTC を BTC チェーン上でボブに送信します。トランザクションがブロックにマイニングされた後、彼女はトランザクションのマークル証明を取得します。これはHLTCを使用しない単純な支払い取引であることに注意してください。
-
検証とロック解除: アリスはマークル証明を BSV 契約に提出します。契約の検証により、ボブに対するアリスの BTC トランザクションが含まれていることを証明および確認します。証拠が検証された場合、アリスは契約のロックを解除して BSV 資金を請求することができます。
ボブが期限内に支払いを受け取らなかった場合、タイムロックの期限が切れた後にコインを取り戻すことができます。
スマート コントラクトは、BTC 支払いがブロードキャストされる前に展開されることに注意してください。こうすることで、アリスはボブに支払った後に正しい金額の BSV を受け取ることが保証されます。このロック時間は、BTC での支払いトランザクションをマイニングするのに十分な長さでなければなりません。
少し変更を加えれば、ボブの契約が展開されるときにアリスを不明にすることもできます。ボブのオファーは公開されており、適切な量の BTC を支払えば誰でも彼のロックされた BSV のロックを解除できます。
HTLC ベースの原子交換との比較
このアプローチの主な利点は、ハッシュ ロックやタイム ロックのないブロックチェーンでもアトミック スワップが可能になることです。トランザクションが含まれていることをトラストレスに証明するメカニズム (たとえば、マークル証明を使用) がある限り、その証明を検証するスマート コントラクト機能を備えた他のチェーンは、その証明を交換できます。
成し遂げる
BSV では、以下に示すようにスマート コントラクトを sCrypt に実装できます。
export type VarIntRes = {
val: bigint
newIdx: bigint
}
class CrossChainSwap2 extends SmartContract {
static readonly LOCKTIME_BLOCK_HEIGHT_MARKER = 500000000
static readonly UINT_MAX = 0xffffffffn
static readonly MIN_CONF = 3
static readonly BTC_MAX_INPUTS = 3
@prop()
readonly aliceAddr: PubKeyHash
@prop()
readonly bobAddr: PubKeyHash
@prop()
readonly bobP2WPKHAddr: PubKeyHash
@prop()
readonly timeout: bigint // Can be a timestamp or block height.
@prop()
readonly targetDifficulty: bigint
@prop()
readonly amountBTC: bigint
@prop()
readonly amountBSV: bigint
// ...
@method()
checkBtcTx(btcTx: ByteString): void {
// Most things should be the same as in BSV except the witness data and flag.
// - Check (first) output is P2WPKH to Bobs public key.
// - Check (first) output amount is equal to this.amountBTC
let idx = 4n
// Make sure to serialize BTC tx without witness data.
// See https://github.com/karask/python-bitcoin-utils/blob/a41c7a1e546985b759e6eb2ae4524f466be809ca/bitcoinutils/transactions.py#L913
assert(
slice(btcTx, idx, idx + 2n) != toByteString('0001'),
'Witness data present. Please serialize without witness data.'
)
INPUTS:
const inLen = CrossChainSwap2.parseVarInt(btcTx, idx)
assert(
inLen.val <= BigInt(CrossChainSwap2.BTC_MAX_INPUTS),
'Number of inputs too large.'
)
idx = inLen.newIdx
for (let i = 0n; i < CrossChainSwap2.BTC_MAX_INPUTS; i++) {
if (i < inLen.val) {
//const prevTxID = slice(btcTx, idx, idx + 32n)
idx += 32n
//const outIdx = slice(btcTx, idx, idx + 4n)
idx += 4n
const scriptLen = CrossChainSwap2.parseVarInt(btcTx, idx)
idx = scriptLen.newIdx
idx += scriptLen.val
//const nSequence = slice(btcTx, idx, idx + 4n)
idx += 4n
}
}
FIRST OUTPUT:
// Check if (first) output pays Bob the right amount and terminate and set res to true.
const outLen = CrossChainSwap2.parseVarInt(btcTx, idx)
idx = outLen.newIdx
const amount = Utils.fromLEUnsigned(slice(btcTx, idx, idx + 8n))
assert(amount == this.amountBTC, 'Invalid BTC output amount.')
idx += 8n
const scriptLen = CrossChainSwap2.parseVarInt(btcTx, idx)
idx = scriptLen.newIdx
const script = slice(btcTx, idx, idx + scriptLen.val)
assert(len(script) == 22n, 'Invalid locking script length.')
assert(
script == toByteString('0014') + this.bobP2WPKHAddr,
'Invalid locking script.'
)
// Data past this point is not relevant in our use-case.
}
@method()
public swap(
btcTx: ByteString,
merkleProof: MerkleProof,
headers: FixedArray<BlockHeader, typeof CrossChainSwap2.MIN_CONF>,
alicePubKey: PubKey,
aliceSig: Sig
) {
// Check btc tx.
this.checkBtcTx(btcTx)
// Calc merkle root.
const txID = hash256(btcTx)
const merkleRoot = MerklePath.calcMerkleRoot(txID, merkleProof)
// Check if merkle root is included in the first BH.
assert(
merkleRoot == headers[0].merkleRoot,
"Merkle root of proof doesn't match the one in the BH."
)
// Check target diff for headers.
for (let i = 0; i < CrossChainSwap2.MIN_CONF; i++) {
assert(
Blockchain.isValidBlockHeader(
headers[i],
this.targetDifficulty
),
`${
i}-nth BH doesn't meet target difficulty`
)
}
// Check header chain.
let h = Blockchain.blockHeaderHash(headers[0])
for (let i = 0; i < CrossChainSwap2.MIN_CONF; i++) {
if (i >= 1n) {
const header = headers[i]
// Check if prev block hash matches.
assert(
header.prevBlockHash == h,
`${
i}-th BH wrong prevBlockHash`
)
// Update header hash.
h = Blockchain.blockHeaderHash(header)
}
}
// Verify Alices signature.
assert(hash160(alicePubKey) == this.aliceAddr, 'Alice wrong pub key.')
assert(this.checkSig(aliceSig, alicePubKey))
}
@method()
public cancel(bobPubKey: PubKey, bobSig: Sig) {
// Ensure nSequence is less than UINT_MAX.
assert(
this.ctx.sequence < CrossChainSwap2.UINT_MAX,
'input sequence should less than UINT_MAX'
)
// Check if using block height.
if (this.timeout < CrossChainSwap2.LOCKTIME_BLOCK_HEIGHT_MARKER) {
// Enforce nLocktime field to also use block height.
assert(
this.ctx.locktime <
CrossChainSwap2.LOCKTIME_BLOCK_HEIGHT_MARKER,
'locktime should be less than 500000000'
)
}
assert(
this.ctx.locktime >= this.timeout,
'locktime has not yet expired'
)
// Verify Bobs signature.
assert(hash160(bobPubKey) == this.bobAddr, 'Bob wrong pub key.')
assert(this.checkSig(bobSig, bobPubKey))
}
}
スマート コントラクトには 2 つのパブリック メソッドがあります。
swap()
: アリスがボブに十分な量の BTC を支払ったという証拠を提供した場合、アリスはこの関数を呼び出して資金を取得します。cancel()
: 指定された時間の後に資金を引き出すためにボブによって呼び出されます。
完全なコードとテストはGitHubにあります。