読者です 読者をやめる 読者になる 読者になる

Develop with pleasure!

福岡でCloudとかBlockchainとか。

relative lock-timeをスクリプトで検証するOP_CSV(BIP-112)

BIP Bitcoin Payment Channel

BIP-112にはCHECKSEQUENCEVERIFYという命令コードが定義されており、BIP-68と組み合わせることで、出力の年齢に基づいた制限を加えることができるようになる。

bips/bip-0112.mediawiki at master · bitcoin/bips · GitHub

概要

CHECKSEQUENCEVERIFYは既存のNOP3 opcodeを再定義する。実行時に以下のいずれかの条件に合致するとインタプリタはエラーで終了する。

  • スタックが空
  • スタックの一番上のアイテムが0以下
  • スタックの一番上のアイテムが無効フラグ(1 << 31)を持っており未設定かつ以下のいずれかの条件を満たす場合

上記以外の場合は、NOPが実行されたかのようにスクリプトの処理を続ける。

BIP-68のRelative lock-timeでは、対応する入力が指定された(ブロック高もしくはブロックタイムで測定される)年齢に達するまで、ブロックに含められないよう制限を加える。
このBIPではnSequenceフィールドに対してCHECKSEQUENCEVERIFYの引数を比較することにより、使用された出力の年齢を間接的に検証する。その相対的な年齢に達するまでCHECKSEQUENCEVERIFYを含む任意のスクリプトの検証は失敗し、そのトランザクションをブロックに含めることはできない。

動機

BIP-68はrelative lock-timeという新しいコンセンサスをシーケンス番号に与えることでnSequenceフィールドを再利用しようとしたが、このフィールドに基いて意思決定するためのスクリプトを作成する方法は定義されていない。

nSequenceフィールドがスクリプトにアクセスすることにより、proof-of-publication*1後に最小時間でアクセスが可能なコードの経路を構築することができるようになる。これによりエスクローやペイメントチャネル、双方向ペグのような多種多様なアプリケーションの構築が可能になる。

有効期限のある契約

タイムアウトのあるエスクロー

資金を供与してから30日後に自動的にタイムアウトになるエスクローは以下の方法で作成することができる。アリスとボブとエスクローは以下のredeemscriptと2-of-3のマルチシグアドレスを作成する。

IF
    2 <アリスの公開鍵> <ボブの公開鍵> <エスクローの公開鍵> 3 CHECKMULTISIGVERIFY
ELSE
    "30d" CHECKSEQUENCEVERIFY DROP
    <アリスの公開鍵> CHECKSIGVERIFY
ENDIF

アリスとボブとエスクローのうち2つの署名があればいつでも資金を使用することができる。

30日経つとアリスの署名だけで使用することができるようになる。

時計はエスクローアドレスへの支払いが確認されるまでスタートしない。

Retroactive Invalidation

これから発生するイベントによっては契約を取り消すことができるような契約を作りたいということが多々ある。しかしブロックチェーンの不変性を考えると、既にconfirmされている以前のコミットメントを遡って無効にすることはできない。(ブロックチェーンを遡って改変するのは基本的なセキュリティの理由上から非常に難しく非常に高価な処理になるよう設計されているため)

こういったブロックチェーンの制限がありながらも、CHECKSEQUENCEVERIFYを使うことで過去のコミットメントの不可逆性を保持しながら無効化の遡及に似た機能を提供することができる。複数の実行ブランチを持つスクリプトを構築し、その内の1つ以上のブランチについて誰かが出力を利用できるようにする無効化の条件を遅れて設定できるtime windowを提供することで、事実上、遅延ブランチを無効化し潜在的に他の関係者にトランザクションをブロードキャストするのを思いとどまらせる。タイムアウトする前に無効化の条件が発生しない場合は、遅延ブランチは当初の契約通り使用可能になる。

このアイディアの具体的な用途としては以下のようなものが考えられる。

Hash Time-Locked Contracts

Hash Time-Locked Contracts (HTLCs)はオフチェーントランザクションの調整のための一般的な仕組みを提供する。無効化期間内に提示される秘密の知識(hash preimage)を必要とする実行経路を作ることができ、その秘密を共有することで、一方は無効化期間が過ぎるまで待たないといけないが、取引相手はすぐにでも出力を要求できるようになる。これにより取引相手に対してトランザクションがブロードキャストされないことを保証することができる。秘密が共有されていない場合は、インスタント経路は使えず代わりに遅延経路を使わなければならない(無効化されるまで待たなければならない)。

双方向ペイメントチャネル

スクリプト可能なrelative locktimeは、取引相手がブロードキャストした取り消しのトランザクションに応答する時間を予測できるようになる。relative locktimeではトランザクションがブロックに含まれてからクロックがスタートするのに対し、既存のlocktime(nLocktime)はチャネルをクローズし、タイムアウトが近づくと再びチャネルを開く必要がある。またチャネルから資金を引き上がるのにどのくらい待てばいいか正確な時間を知るための手段を提供する。

Lightning Network

lightning networkは双方向ペイメントチャネルを拡張するアイディアで、複数のペイメントチャネルを介してルーティングされる支払いを可能にする。

これらのチャネルはアリスとボブの2-of-2のマルチシグと一連の取り消し可能なコミットメントトランザクションを必要とするアンカートランザクションに基いている。コミットメントトランザクションはアリスとボブ間で固定された資金を分割し、最新のコミットメントトランザクションがどちらかによって任意の時点で公開され、チャネルを閉じる。

理想的には、取り消されたコミットメントトランザクションを使用することはできず、最新のコミットメントトランザクションは迅速に使用することができる。

コミットメントトランザクションを効果的に取り消せるようにするため、アリスとボブは最新のコミットメントトランザクションのバージョンとわずかに異なるバージョンを持っている。アリスのバージョンでは、コミットメントトランザクションの任意の出力は、遅れてアリスへ支払われ、代替ブランチではボブが取消コードを知っている場合はその出力を使用できる。
ボブのバージョンでは、ボブへの支払いを同様に阻害する。アリスとボブが新しい取引と金額について調整する際、古いトランザクションを中継しないよう前の取消コードを明らかにする。

アリスへ支払いを行う単純な出力は以下のようになる。

HASH160 <revokehash> EQUAL
IF
    <Bob key hash>
ELSE
    "24h" CHECKSEQUENCEVERIFY DROP
    <Alice key hash>
ENDIF
CHECKSIG

↑ではアリスはいつでも最新のコミットメントトランザクションを公開し24時間後にその資金を利用できるようになるが、アリスが取消トランザクションをリレーした場合、ボブは24時間は資金を請求できる。

CHECKLOCKTIMEVERIFYを使うと以下のように書ける。

HASH160 <revokehash> EQUAL
IF
    <Bob key hash>
ELSE
    "2015/12/15" CHECKLOCKTIMEVERIFY DROP
    <Alice key hash>
ENDIF
CHECKSIG

↑のトランザクションの形式は、アンカーが2015/12/15時点で未使用であればアリスはコミットメントが失効している場合でもそのコミットメントを使用することができ、速やかにそれを使用することでボブに請求するための時間を与えないようにすることもできる。

これはチャネルが期限を持つことを意味し、期限が過ぎるまで資金が利用できない可能性がある。CHECKSEQUENCEVERIFYを利用することでこういったトレードオフを回避できる。

Hashed Time-Lock Contracts (HTLCs) はこれを少し複雑にする。原則としてアリスがsecret Rを見つけるか、タイムアウトに達するかで同じ原則が適用される(アリスのコミットメントトランザクション内のアリスへの支払いは遅延し、Revocation secretが知られている場合、出力は他の当事者が使用することができる)。
CHECKSEQUENCEVERIFYを使ってHTLCsのアリスへの支払いは以下のようなアリストのコミットメントトランザクションになる。

HASH160 DUP <R-HASH> EQUAL
IF
    "24h" CHECKSEQUENCEVERIFY
    2DROP
    <Alice key hash>
ELSE
    <Commit-Revocation-Hash> EQUAL
    NOTIF
        "24h" CHECKLOCKTIMEVERIFY DROP
    ENDIF
    <Bob key hash>
ENDIF
CHECKSIG

そして対応するボブのコミットメントトランザクションは↓

HASH160 DUP <R-HASH> EQUAL
SWAP <Commit-Revocation-Hash> EQUAL ADD
IF
    <Alice key hash>
ELSE
    "2015/10/20 10:33" CHECKLOCKTIMEVERIFY
    "24h" CHECKSEQUENCEVERIFY
   2DROP
   <Bob key hash>
ENDIF
CHECKSIG
サイドチェーンへの2way peg

サイドチェーンへの2way pegにはREORGPROOFVERIFYという新しい命令コードが必要になる(REORGPROOFVERIFYについてはこのBIPの対象外)。2way pegの戻りのpegが再編成の証明を発行するためにポストされてから十分時間が経過しているかどうか確認する際に、CHECKSEQUENCEVERIFYが使われる。

IF
    lockTxHeight <lockTxHash> nlocktxOut [<workAmount>] reorgBounty Hash160(<...>) <genesisHash> REORGPROOFVERIFY
ELSE
    withdrawLockTime CHECKSEQUENCEVERIFY DROP HASH160 p2shWithdrawDest EQUAL
ENDIF

仕様

以下、参照実装

/* Below flags apply in the context of BIP 68 */
/* If this flag set, CTxIn::nSequence is NOT interpreted as a
 * relative lock-time. */
static const uint32_t SEQUENCE_LOCKTIME_DISABLE_FLAG = (1 << 31);

/* If CTxIn::nSequence encodes a relative lock-time and this flag
 * is set, the relative lock-time has units of 512 seconds,
 * otherwise it specifies blocks with a granularity of 1. */
static const uint32_t SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22);

/* If CTxIn::nSequence encodes a relative lock-time, this mask is
 * applied to extract that lock-time from the sequence field. */
static const uint32_t SEQUENCE_LOCKTIME_MASK = 0x0000ffff;
   
case OP_NOP3:
{
    if (!(flags & SCRIPT_VERIFY_CHECKSEQUENCEVERIFY)) {
        // not enabled; treat as a NOP3
        if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS) {
            return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS);
        }
        break;
    }

    if (stack.size() < 1)
       return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

    // Note that elsewhere numeric opcodes are limited to
    // operands in the range -2**31+1 to 2**31-1, however it is
    // legal for opcodes to produce results exceeding that
    // range. This limitation is implemented by CScriptNum's
    // default 4-byte limit.
    //
    // Thus as a special case we tell CScriptNum to accept up
    // to 5-byte bignums, which are good until 2**39-1, well
    // beyond the 2**32-1 limit of the nSequence field itself.
    const CScriptNum nSequence(stacktop(-1), fRequireMinimal, 5);

    // In the rare event that the argument may be < 0 due to
    // some arithmetic being done first, you can always use
    // 0 MAX CHECKSEQUENCEVERIFY.
    if (nSequence < 0)
        return set_error(serror, SCRIPT_ERR_NEGATIVE_LOCKTIME);

    // To provide for future soft-fork extensibility, if the
    // operand has the disabled lock-time flag set,
    // CHECKSEQUENCEVERIFY behaves as a NOP.
    if ((nSequence & CTxIn::SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0)
        break;

    // Compare the specified sequence number with the input.
    if (!checker.CheckSequence(nSequence))
        return set_error(serror, SCRIPT_ERR_UNSATISFIED_LOCKTIME);

    break;
}
    
bool TransactionSignatureChecker::CheckSequence(const CScriptNum& nSequence) const
{
    // Relative lock times are supported by comparing the passed
    // in operand to the sequence number of the input.
    const int64_t txToSequence = (int64_t)txTo->vin[nIn].nSequence;

    // Fail if the transaction's version number is not set high
    // enough to trigger BIP 68 rules.
    if (static_cast<uint32_t>(txTo->nVersion) < 2)
        return false;

    // Sequence numbers with their most significant bit set are not
    // consensus constrained. Testing that the transaction's sequence
    // number do not have this bit set prevents using this property
    // to get around a CHECKSEQUENCEVERIFY check.
    if (txToSequence & CTxIn::SEQUENCE_LOCKTIME_DISABLE_FLAG)
        return false;

    // Mask off any bits that do not have consensus-enforced meaning
    // before doing the integer comparisons
    const uint32_t nLockTimeMask = CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG | CTxIn::SEQUENCE_LOCKTIME_MASK;
    const int64_t txToSequenceMasked = txToSequence & nLockTimeMask;
    const CScriptNum nSequenceMasked = nSequence & nLockTimeMask;

    // There are two kinds of nSequence: lock-by-blockheight
    // and lock-by-blocktime, distinguished by whether
    // nSequenceMasked < CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG.
    //
    // We want to compare apples to apples, so fail the script
    // unless the type of nSequenceMasked being tested is the same as
    // the nSequenceMasked in the transaction.
    if (!(
        (txToSequenceMasked <  CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG && nSequenceMasked <  CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) ||
        (txToSequenceMasked >= CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG && nSequenceMasked >= CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG)
    ))
        return false;

    // Now that we know we're comparing apples-to-apples, the
    // comparison is a simple numeric one.
    if (nSequenceMasked > txToSequenceMasked)
        return false;

    return true;
}

github.com

まとめ&所感

  • BIP-68のRelative-Lock-TimeがnSequenceにセットされた時間まで、そのトランザクションのマイニングを禁止するのに対し、OP_CSVスクリプト内にRelative Lock TimeとOP_CSVスクリプトを記載することで、マイニングされてからLocktimeで指定した時間までコインをロックするというもの。
  • OP_CSVは入力のシーケンス番号とOP_CSVの引数を比較することでその入力の年齢を検証する。
  • BIP-68のrelative lock-timeではトランザクションの入力に設定されるnSequenceにlocktimeを設定する仕様が定義されているが、BIP-112ではそれをスクリプトで検証するために新しい命令コードを定義したもの。
  • OP_CSVを利用することでトランザクションで実現できることの幅が広がるので、いろんな応用パターンがありそう。
  • OP_CSVとは直接関係ないけど、2way pegを実現するために新しく命令コードREORGPROOFVERIFYが追加されるのね。今のところREORGPROOFVERIFYに関するBIPは無いみたい。

*1:特定の情報が特定の日付で公開されたことを証明する