Develop with pleasure!

福岡でCloudとかBlockchainとか。

Output Script Descriptorの基本仕様を定義したBIP-380

Output Script Descriptorは、Bitcoin Core v0.17からサポートされ始めた言語で、ウォレットやその他のプログラムが、自身が所有(関連)するUTXOを追跡するのに必要な情報を含むデータを定義する。

Bitcoinのウォレットの多くはHDウォレットをサポートしており、基本的には12個 or 24個からなるニーモニックをバックアップしておけば、マスターシードが復元でき、ウォレットが所有するUTXOを認識できる。

ただ、Segwit(P2WPKH、P2WSH)を始めマルチシグや、11月にアクティベート予定のTaprootへの支払いであるP2TRアドレスなど、1つの鍵が取るアドレス(アウトプット)の形態が増えてきたため、単純に鍵を復元しただけでは、どのアドレスを復元するのか判断できなくなっている。

そのため、ウォレット間のインターオペラビリティを担保するために、アウトプット(アドレス)の導出方法まで含めてウォレットのバックアップができるようにOutput Script Descriptorを利用しようと、今回新しくOutput Script DescriptorのBIPが定義された↓

https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki

BIP-380で定義されているのは、Output Script Descriptorの基本コンセプトと、ベースとなるScirpt Experession、Key Expressionとチェックサムの仕様について。その他のExpressionについては後続のBIPで定義されている。

まぁ、でもウォレットのエクスポート形式がOutput Script Descriptorになって、アドレス追加する度にウォレットのバックアップが必要になるのは、マスターシードさえバックアップしておけばいい現状の仕組みと比べて不便だな。それとも/*みたいな指定方法で回避するようになるのかな?

以下、BIP-380の意訳↓

概要

Output Script Descriptorは、Output Scriptのコレクションを記述するために使用できるシンプルな言語である。さまざまなdescriptorの断片や関数がある。このドキュメントでは、descriptorの一般的な構文、チェックサムおよび共通の式について記載する。

動機

Bitcoinウォレットは、従来、鍵のセットを保存していた。これらの鍵は、後でシリアライズされ、ウォレットが監視するOutpu Scriptやユーザーに提供するアドレスを生成するのに使われる。通常、バクアップは秘密鍵のみで構成され、最近では、BIP39のニーモニックの形で保存されている。しかし、Segregated Witnessが導入され、新たなアウトプットタイプが追加されてからは、このバックアップ方法では不十分だ。秘密鍵だけでは、復元されたウォレットがどのような種類のOutput Scriptやアドレスを生成するか知ることはできない。そのため、バックアップを復元したり、監視用ウォレットのデータをエクスポートする際に、ウォレット間で互換性がないという問題があった。

さらに問題を複雑にしているのがBIP32の導出パスだ。BIP44, 49, 84では、さまざまなOutput Scriptやアドレスのための標準的なBIP32導出パスが定義されているが、すべてのウォレットがそれらをサポートしている訳ではなく、それらの導出パスを使っているわけでもない。これらのバックアップやエクスポートに導出パスの情報が含まれていないため、ウォレット間の互換性が失われてしまう。

これらの問題に対する現在の解決策は、一般的なものではなく、レイヤー違反とみなすことができる。拡張鍵のシリアライゼーションに異なるversion byteを導入するような解決策は、レイヤー違反であり(鍵の導出はスクリプトタイプの意味とは分離されるべき)、特定の導出パスとスクリプトタイプにのみ特有のものだ。

Output Script Descriptorは、これらの問題に対する一般的な解決策を導入する。スクリプトタイプはScript Expressionを用いて明示的に指定される。鍵の導出パスはKey Expressionで明示的に指定する。これにより、作成するスクリプトやサブスクリプト(redeemScriptやwitnessScriptなど)および鍵を正確に指定したウォレットのバックアップおよびエクスポートを作成できる。本BIPで指定された一般的な構造により、新しいスクリプトタイプが追加されると、新しいScript Expressionを導入することができる。最後に、共通の用語と既存の規格を使用することで、Output Script Descriptorを技術的に読みやすくし、結果を一目で理解できるようにしている。

仕様

Descriptorはいくつかのタイプの式で構成される。最上位の式はSCRIPT。この式の後には#CHECKSUMが続く。ここでCHECKSUMは8文字の英数字のdescriptorのチェックサム

Script Expression

Script Expression(SCRIPTと表記)は、Bitcoinスクリプトに直接対応する式。これらの式は関数として記述され、引数を取る。このような式には、対応する引数を満たすスクリプトテンプレートがある。式は人が読める識別子文字列で書かれ、引数は括弧で囲まれている。識別子の文字列は英数字で、アンダースコアを含むことができる。

Script Expressionの引数は、その式自体によって定義される。Script Expression、Key Expressionまたはその他の式である可能性がある。

Key Expression

Script Expressionの引数としてよく使われる式に、Key Expression(KEYと表記)がある。これは公開鍵または秘密鍵と、オプションとしてその鍵のオリジンに関する情報を表す。Key Expressionは、Script Expressionの引数としてのみ使用することができる。

Key Expressionは以下の要素で構成される:

  • オプションで、以下で構成されるキーオリジンの情報:
    • 開き括弧[
    • 導出を開始する鍵のfingerprintを表す正確な8つのhex文字(詳細はBIP-32参照)
    • fingerprintとそれに続く鍵との間の、非強化導出または強化導出ステップを示す0個以上の/NUMもしくは/NUMhパス要素。
    • 閉じ括弧]
  • 次のいずれかである実際の鍵が続く:
    • hexエンコードされた公開鍵で、Script Expressionに応じて、次のいずれかになる:
      • 圧縮公開鍵を表す02または03で始まる66個のhex文字列
      • 非圧縮公開鍵を表す04で始まる130個のhex文字列
    • WIFエンコードされた秘密鍵
    • xpubエンコードされた拡張公開鍵、もしくはxprvエンコードされた拡張秘密鍵(詳細はBIP-32参照)
      • 拡張鍵の後に、実行されるBIP32導出ステップを示す0個以上の/NUMもしくは/NUMhパス要素が続く。
      • オプションで、最後のステップで単一の/*または/*hが続く。これはすべての直接的な非強化もしくは強化導出される子を示す。

KEYがBIP32拡張鍵の場合、Output Scriptを作成する前に、拡張鍵の後に続く導出情報を使って子鍵を導出する必要がある。最終ステップが/*または/*hの場合は、各子鍵のインデックス毎にOutput Scriptが作成される。導出した鍵は、非圧縮公開鍵としてシリアライズしてはならない。Script Expressionでは、導出した公開鍵をスクリプト作成のためどうシリアライゼーションするかについて、追加要件が定められている場合がある。

上記の仕様では、強化導出を示すhは、代替表現であるH'に置き換えることができる。

強化導出を伴うKey Expressionの正規化

秘密鍵なしでdescriptorがエクスポートされる場合、エクスポートされたdescriptorが有用であるためには、中間の強化導出ステップを削除するために追加の導出を行う必要がある。エクスポーターは、最後の強化導出ステップで拡張公開鍵を導出し、その拡張公開鍵をdescriptorの鍵として使用しなければならない。その鍵を導出するまでに行われた導出ステップは、前の鍵のオリジン情報に追加されなければならない。鍵のオリジン情報がない場合、新たに導出した拡張公開鍵のために追加しなければならない。最終的な導出が強化導出の場合、追加の導出は必要ない。

文字セット

descriptorで使用される式には、descriptorのチェックサムが機能するように、この文字セット内の文字のみが含まれている必要がある。

許可される文字は:

0123456789()[],'/*abcdefgh@:$%{}
IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~
ijklmnopqrstuvwxyzABCDEFGH`#"\<space>

最後の行の<space>はスペース文字であることに注意。

この文字セットは、以下のチェックサムがより多くのエラーを識別できるように、32文字の3つのグループとして特定の順序で書かれている。最初のグループは最も一般的な「保護されていない」文字(つまり、hexやkeypathなどの独自のチェックサムを持たないもの)。大文字小文字のエラーは32の倍数のオフセットを発生させるが、その一方で、前の制限に従い、できるだけ多くのアルファベット文字が同じグループに入っている。

チェックサム

トップレベルのScript Expressionの後には、単一のナンバー記号(#)とそれに続く8文字のチェックサムが続く。チェックサムはbech32と同様のエラー訂正チェックサムだ。

チェックサムには以下の特性がある:

  • descriptor文字列の間違いは、「シンボルエラー」で測定される。シンボルエラーの数が多いほど、検出が難しくなる:
    • 0123456789()[],'/*abcdefgh@:$%{}の文字を、そのセット内の別の文字に置き換えるエラーは、常に1シンボルエラーとしてカウントされる。
      • hexエンコードされた鍵はこれらの文字でカバーされることに注意してほしい。拡張鍵(xpubおよびxprv)は他の文字も使用するが、独自にチェックサムの仕組みもある。
      • SCRIPT式の関数名は、他の文字を使用する。これらの文字を間違えると、通常descriptorをパースできりなくなる。
    • 大文字小文字のエラーは常に1シンボルエラーとしてカウントされる。
    • その他の1文字の置換エラーは、1つまたは2つのシンボルエラーとしてカウントされる。
  • 1シンボルエラーは常に検出される。
  • 最大49154文字までのdescriptorの2または3シンボルエラーは常に検出される。
  • 最大507文字までのdescriptorの4シンボルエラーは常に検出される。
  • 最大77文字までのdescriptorの5シンボルエラーは常に検出される。
  • 最大387文字までのdescriptorで5シンボルエラーが検出されない可能性を最小限に抑えるよう最適化されている。
  • ランダムエラーは、 {1/2^{40}}で検出されない可能性がある。

チェックサム自体はbech32と同じ文字セットqpzry9x8gf2tvdw0s3jn54khce6mua7lを使用する。

チェックサム付き有効なdescriptor文字列は、以下のPython3のコードスニペットで指定された有効性の基準に合格する必要がある。descsum_check関数は、引数sSCRIPT#CHECKSUM形式で構成されるdescriptorである場合にtrueを返す。

INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
GENERATOR = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd]

def descsum_polymod(symbols):
    """Internal function that computes the descriptor checksum."""
    chk = 1
    for value in symbols:
        top = chk >> 35
        chk = (chk & 0x7ffffffff) << 5 ^ value
        for i in range(5):
            chk ^= GENERATOR[i] if ((top >> i) & 1) else 0
    return chk

def descsum_expand(s):
    """Internal function that does the character to symbol expansion"""
    groups = []
    symbols = []
    for c in s:
        if not c in INPUT_CHARSET:
            return None
        v = INPUT_CHARSET.find(c)
        symbols.append(v & 31)
        groups.append(v >> 5)
        if len(groups) == 3:
            symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2])
            groups = []
    if len(groups) == 1:
        symbols.append(groups[0])
    elif len(groups) == 2:
        symbols.append(groups[0] * 3 + groups[1])
    return symbols

def descsum_check(s):
    """Verify that the checksum is correct in a descriptor"""
    if s[-9] != '#':
        return False
    if not all(x in CHECKSUM_CHARSET for x in s[-8:]):
        return False
    symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]]
    return descsum_polymod(symbols) == 1

これにより、上記の特性を持つBCH符号が実装される。descriptor文字列全体が最初に処理されシンボルの配列になる。各文字のシンボルはそのグループ内での位置。3つめのシンボルごとにグループ番号を組み合わせた4つめのシンボルが挿入される。これは、グループ内の位置にのみ影響する変更、もしくはグループ番号の変更にのみ影響する変更は、単一のシンボルのみに影響することを意味する。

Script Expressionを指定して有効なチェックサムを作成する場合、以下のコードを使用できる:

def descsum_create(s):
    """Add a checksum to a descriptor without"""
    symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
    checksum = descsum_polymod(symbols) ^ 1
    return s + '#' + ''.join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8))

後方互換

Output Script Descriptorは、まったく新しい言語であり、既存のソフトウェアとは互換性がない。ただし式の多くのコンポーネントはこれまでのBIPで定義されたエンコーディングとシリアライゼーションを再利用する。

Output Script Descriptorは、将来の拡張用に、あらにセグメントタイプと新しいScript Expressionを使って設計されている。これらは追加のBIPに定義される。

参照実装

descriptorは、バージョン0.17以降のBitcoin Coreに実装されている。

Appendix A:式のインデックス

将来のBIPは、追加の式のタイプを指定する可能性がある。この表には、使用可能なすべての式タイプがリストされている。

名称 記述 BIP
Script SCRIPT 380
Key KEY 380
Tree TREE 386

Appendix B: Script Expressionのインデックス

Script Expressionは追加のBIPで定義される。この表は使用可能なScript Expressionとそれを定義したBIPのリスト。

BIP
pk(KEY) 381
pkh(KEY) 381
sh(SCRIPT) 381
wpkh(KEY) 382
wsh(SCRIPT) 382
multi(NUM, KEY, ..., KEY) 383
sortedmulti(NUM, KEY, ..., KEY) 383
combo(KEY) 384
raw(HEX) 385
addr(ADDR) 385
tr(KEY), tr(KEY, TREE) 386