Tech

Counterparty プロトコルにおけるデータ保存方法

  • このエントリーをはてなブックマークに追加

細かいことはドキュメントに書いてなかったので、コードを読んでまとめました。

まとめ

  • ビットコインのトランザクションの中にデータを入れている
  • 過去、色々なデータフォーマットがあった
  • エンコーディングの種類と、それぞれの種類でデータのバージョンがあった
  • 現在のデフォルトは、1-of-3 Multi-sig
  • Multi-sigの場合、2つのニセ公開鍵の中にデータを入れている

前提知識

  • 技術的な基礎知識
  • ビットコインの技術的知識
    • アドレスがどのように作られているのか? (pubkey, pukeyhash, UTXO, uxidなど)
    • Scriptというスクリプト言語を読める
    • Multi-sig の知識
    • Multi-sig そのScriptの形(OPコード)
  • Counterpartyの基礎知識
    • bitcoind, counterpartydがどう動いているのか、など

3つのエンコーディングの種類

  1. OP_RETURN として返す場合、opreturnをエンコーディング・パラメータとして指定する
    • bitcoind 0.9.0以降40byte制限があるので、全てのCounterpartyトランザクションが可能なわけではない
  2. multisigトランザクションとして返す場合、multisig をエンコーディング・パラメータとして指定する
    • auto は、今のところ multisig を指定しているのと同じ
  3. pubkeyhash の場合、 pubkey を指定する
    • UTXO setを汚染するので、推奨しない

ARC4

送信者の一つめのtxid(公開鍵ではない?)をパスワードとして、2つ目3つ目を暗号化する

データの保存方法

  • エンコード: UTF-8
  • プリフィックス: CNTRPRTY
  • 3種類のアウトプットがある
  • データは、ARC4で暗号化
  • 1-of-3 multi-sig
  • 3つの内の最初の2が、ニセ公開鍵(データ)
  • 残りの1つが、元の公開鍵
  • 0でパディング

Multi-sig の Script

こんな感じで構築

tx_script = OP_1                                   # OP_1
tx_script += op_push(33)                           # Push bytes of data chunk (fake) public key    (1/2)
tx_script += data_pubkey_1                         # (Fake) public key                  (1/2)
tx_script += op_push(33)                           # Push bytes of data chunk (fake) public key    (2/2)
tx_script += data_pubkey_2                         # (Fake) public key                  (2/2)
tx_script += op_push(len(dust_return_pubkey))  # Push bytes of source public key
tx_script += dust_return_pubkey                       # Source public key
tx_script += OP_3                                  # OP_3
tx_script += OP_CHECKMULTISIG                      # OP_CHECKMULTISIG

CounterpartyXCP/counterpartyd

データ・フォーマット

# Counterparty protocol
TXTYPE_FORMAT = '>I'

Python3 の struct.pack, struct.unpackを使っている
Python 上で文字列データとして表される C の構造体データの変換をする

issuranceの場合

FORMAT_1 = '>QQ?'
LENGTH_1 = 8 + 8 + 1
FORMAT_2 = '>QQ??If'
LENGTH_2 = 8 + 8 + 1 + 1 + 4 + 4 

データ・フォーマットが何回か変更された模様

FORMAT_1の場合

  1. Asset ID (整数)
  2. Quantity (整数)
  3. Divisible (bool)
            if len(message) != LENGTH_1:
                raise exceptions.UnpackError
            asset_id, quantity, divisible = struct.unpack(FORMAT_1, message)
            callable_, call_date, call_price, description = False, 0, 0.0, ''

FORMAT_2の場合

  1. Asset ID
  2. Quantity
  3. Divisible
  4. callable_
  5. call_date
  6. call_price
  7. Description
            asset_id, quantity, divisible, callable_, call_date, call_price, description = struct.unpack(curr_format, message)

            call_price = round(call_price, 6) # TODO: arbitrary

Counterpartyで使われるフォーマット

  • “>” は、ビッグエンディアンの意味
  • フォーマット文字列の前に整数がついているのは、繰り返し
フォーマット C での型 Python 型 標準のサイズ 備考
Q unsigned long long 整数型 8 (2), (3)
? _Bool 真偽値型(bool) 1 (1)
I unsigned int 整数型 4 (3)
f float 浮動小数点型 4 (4)
H unsigned short 整数型 2 (3)
s char[] 文字列

フォーマットのコード内の例

https://github.com/CounterpartyXCP/counterpartyd/blob/master/lib/messages/issuance.py#L13

トランザクションのパース

def parse_tx(db, tx): の中で mesage_type_idを取得して、そこからデータの形を決めて、パース

    if len(tx['data']) > 4:
        try:
            message_type_id = struct.unpack(config.TXTYPE_FORMAT, tx['data'][:4])[0]
        except struct.error:    # Deterministically raised.
            message_type_id = None
    else:
        message_type_id = None


上記の続き。
message_type_idを見て、メッセージの種類によって個別にパース

 # Protocol change.
    rps_enabled = tx['block_index'] >= 308500 or config.TESTNET

    message = tx['data'][4:]
    if message_type_id == send.ID:
        send.parse(db, tx, message)
    elif message_type_id == order.ID:
        order.parse(db, tx, message)
    elif message_type_id == btcpay.ID:
        btcpay.parse(db, tx, message)
    elif message_type_id == issuance.ID:
        issuance.parse(db, tx, message)
    elif message_type_id == broadcast.ID:
        broadcast.parse(db, tx, message)
    elif message_type_id == bet.ID:
        bet.parse(db, tx, message)
    elif message_type_id == dividend.ID:
        dividend.parse(db, tx, message)
    elif message_type_id == cancel.ID:
        cancel.parse(db, tx, message)
    elif message_type_id == rps.ID and rps_enabled:
        rps.parse(db, tx, message)
    elif message_type_id == rpsresolve.ID and rps_enabled:
        rpsresolve.parse(db, tx, message)
    elif message_type_id == publish.ID and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX:
        publish.parse(db, tx, message)
    elif message_type_id == execute.ID and tx['block_index'] != config.MEMPOOL_BLOCK_INDEX:
        execute.parse(db, tx, message)
    elif message_type_id == destroy.ID:
        destroy.parse(db, tx, message)
    else:

API

  • 全ての create_ API は、署名されてないhexエンコードされた生トランザクション・stringを返す

メッセージ内コンポーネント

  • 送信元アドレス
  • 送信先アドレス(optional)
  • 送信元から送信先に送られるビットコインの量 (もし存在するなら)
  • 送信料金。ビットコイン。ブロックにトランザクションを入れるビットコインマイナーに支払われる。
  • ‘データ’。 特別に作られたトランザクション・アウトプットに組み込まれている。

Assetのデータ

Protocol Specification | Counterparty

  • Asset name
  • Asset ID
  • Description
  • Divisiblity
  • Callability
  • Call date (if callable)
    may be delayed with later issuances
  • Call price (if callable) (non‐negative) may be increased with later issuances

メッセージの種類

  • Send
  • Order
  • BTCPay
  • Issue
  • Broadcast
  • Bet
  • Dividend
  • Burn
  • Cancel

どのようにデータは保存されているか?

以下のフォーラムによると
Detect Counterparty transaction – Development & Technical Discussions on Counterparty Forums

  • UTF-8 の CNTRPRTY の prefixで始まる
  • データは、3種類のアウトプットに保存される
  • 全てのデータは、最初の送信者の公開鍵をパスワードにしたARC4の暗号化で難しくしている
  • Multisig の 1-of-3 の最初の公開鍵は送信者のもの
    • 残り2つの公開鍵は、エンコードされたデータ(0でパディング、length byte でプリフィックス)

コードを見る限り、上記一部間違っているようです。

Counterparty勉強会

プレゼン資料

英語です。以下の動画で日本語で説明しています。
http://crypto-tech.jp/counterparty-doc/#/

YouTube 動画

Counterparty技術勉強会 – YouTube

参考記事

CounterpartyのNumeric Assetを作成する – @yzono blog

検索用語

  • ビットコイン、カウンターパーティ
  • Bitcoin, Bitcoin 2.0, Counterparty, Counterwallet
  • このエントリーをはてなブックマークに追加