Develop with pleasure!

福岡でCloudとかBlockchainとか。

Confidential Transactionのcommitmentを生成してみる

以前、Confidential Transactionのホワイトペーパーを読んだ↓

techmedia-think.hatenablog.com

Confidential Transactionでは、トランザクションの出力にコインの量をセットする代わりに、Pedersen commitmentを楕円曲線に適用したコミットメント(この場合は楕円曲線上のある点)をセットすることで、トランザクションで取引されるコインの量を秘匿している。

ホワイトペーパーを読んで概要はなんとなく分かったまま放置してたので、実際にコードで検証してみる。

commitmentの生成

このコミットメントは

commitment = xG + aH

の式で定義され、xが量の秘匿のために用いられるblinding factorでいわゆるシークレットに該当する。実際に取引するコインの量はaになる。
この式はxGからなる楕円曲線上の点と、aHからなる楕円曲線上の点を加算した新たな点を算出し、この点をコインの量の代わりにトランザクションの出力にセットしている。

Hの算出

ここでG楕円曲線のベースポイントで、HGエンコードして生成するもう1つのベースポイントである。Hは以下の式で楕円曲線上のX座標を計算し、そこからY座標を求めることで算出でき、これも楕円曲線上の点になる。

HのX座標 = SHA256(ENCODE(G))

Rubyecdsa gemを使うとこのX座標は以下のように算出できる。

require 'ecdsa'
require 'securerandom'

G = ECDSA::Group::Secp256k1

encoded_g = ECDSA::Format::PointOctetString.encode(G.generator)
coordinate_x = Digest::SHA256.hexdigest(encoded_g).to_i(16)
=> 36444060476547731421425013472121489344383018981262552973668657287772036414144

X座標が分かれば、secp256k1の楕円曲線上のY座標は以下のように算出できる。ただ楕円曲線はX軸関して対称であるため、Y座標の候補は2つある。

P = G.field.prime

coordinate_y1 = G.field.power((coordinate_x**3 + 7) % P, (P + 1)/4)
=> 93254584761608041185240733468443117438813272608612929589951789286136240436011

coordinate_y2 = coordinate_y1 * -1 % P
=> 22537504475708154238330251540244790414456712057027634449505794721772594235652

Confidential Transactionを実装しているBlockstreamのサイドチェーンElements Alphaのソースを確認すると後者のY座標の値が使われていたので、そちらを使用して、Hの楕円曲線上の点は以下の座標になる。

  • X座標:36444060476547731421425013472121489344383018981262552973668657287772036414144
  • Y座標:22537504475708154238330251540244790414456712057027634449505794721772594235652
# 2つめのベースポイントH
H = ECDSA::Point.new(G, coordinate_x, coordinate_y2)

ちなみにこのHをGから導出する固定値でなく、アセットを表す識別子としてアセット毎に任意に生成できるようにしたのがConfidential Assets

Hの算出方法を確認するため↑の計算をしたけど、Hは固定値なので以下のように定義してそのまま使うのでも良い。

# ECDSA::Grou::Secp256k1 = G とgenerP = G.field.primeatorのポイントが違うだけのGroupを定義
module ECDSA
  class Group
    Secp256k1H = new(
        name: 'secp256k2',
        p: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F,
        a: 0,
        b: 7,
        g: [0x50929b74_c1a04954_b78b4b60_35e97a5e_078a5a0f_28ec96d5_47bfee9a_ce803ac0,
            0x31d3c686_3973926e_049e637c_b1b5f40a_36dac28a_f1766968_c30c2313_f3a38904],
        n: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141,
        h: 1,
    )
  end
end

コミットメントの作成

blinding factorをx、取引するsatoshiの量をaとした時、そのコミットメントは以下のように算出できる。

# blinding factor
x = 1 + SecureRandom.random_number(G.order - 1)

# コミットするコインの量
a = 10000

# コミットメント
commitment = G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(a)

どうしてGHの2つのベースポイントが必要かというと、もしHでなく同じGを使うと、blinding factorの値によって実際のコインの量をなんとでもコントロール出来てしまうからで、そのためblinding factorとコインの量を表現する際のベースポイントは必ず別のポイントである必要がある。

秘匿された量の検証

Confidential Transactionでは、↑のように出力にセットするのが明示的な量ではなく、コミットメント=楕円曲線上の点になる。この場合実際にいくらの量がやりとりされているかは、blinding factorの値を知る取引の当事者しか分からず、blinding factorを知らない他の参加ノードは実際の取引量は分からない。

ただ、取引量がわからなくても、トランザクションの入力と出力でコインの量が正しくセットされているか=入力に使われているコインの量を超えるような出力がセットされていないかについては、blinding factorを知っている・知っていないに関らず誰もが検証できる必要がある。

この検証は、トランザクションの入力に使われている全コミットメントと出力の全コミットメントに対し以下の式が成立すれば入力と出力でコインの量は等しいことが確認できる。

(入力1のコミットメント + 入力2のコミットメント... ) - (出力1のコミットメント + 出力2のコミットメント...) = 0

ただこの時、手数料が問題になる。Bitcoinトランザクションでは入力-出力の差が手数料として使用されるが、Confidential Transactionのように量が秘匿されている状態では手数料がいくらかは分からないので、手数料は明示的にトランザクションにセットする必要がある。

実際にこの式が成立するか検証してみよう。とりあえず手数料を考えず、全入力のコミットメントを加算した点から全出力のコミットメントを加算した点を引いたものが0になれば良い。

楕円曲線上の点の減算は、減算したい点についてX軸について対称の点を計算し、その点を加算することが減算になる。(x, y)に対して(x, -y)を作れば前者の点を使った減算ができる。ちなみにこの両者を加算すると無限遠点となり、この場合、無限遠点はこの演算に関してゼロとして機能する。そのため、全入力のコミットメントから全出力のコミットメントを減算した結果、無限遠点となればイコール0と判断できる。

1500のコインをもつ1つの入力と、1000と500の2つの出力を例に計算してみる。

この場合入力のコミットメントは

# blinding factor
x = 1 + SecureRandom.random_number(G.order - 1)

input =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1500)

となる。同じxを使った出力のコミットメントは単純に考えると以下のようになる

output1 =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1000)
output2 =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(500)

が、この出力は正しくない。このコミットメントを使って全入力のコミットメント-全出力コミットメントを計算するとG.generator.multiply_by_scalar(x)の分だけ余りが発生し0にはならない。

そのため、こういうケースでは計算が合うように出力のblinding factorの値を選択する必要がある。
今回は簡易的に計算するため、それぞれのflinding factorを以下のように調整した値のコミットメントを作成した。

output1 =  G.generator.multiply_by_scalar(x / 2) + H.multiply_by_scalar(1000)
output2 =  G.generator.multiply_by_scalar(x - x / 2) + H.multiply_by_scalar(500)

実際にblinding factorをどのように設定するかは、トランザクションを作成するユーザーが全入力のコミットメント-全出力コミットメント=0になれば後は自由に決めて良い。

実際にこのコミットメントを持つトランザクションが与えられた際、当事者以外は以下の検証をすると入力と出力が同じ量であるか検証できる。

input =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1500)

output1 =  G.generator.multiply_by_scalar(x / 2) + H.multiply_by_scalar(1000)
output2 =  G.generator.multiply_by_scalar(x - x / 2) + H.multiply_by_scalar(500)

outputs = output1 + output2

result = input + outputs.negate # ECDSA::Point#negateは、その点のX軸に対して対象な点を算出するメソッド

result.infinity?
=> true

実際に入力と出力の関係が1:1になることはほとんど無いと思うので、入力のblinding factorとその入力が持つ量と出力先の数を踏まえて、新たに出力で使用するbliding factorを計算する必要がある。