圧縮/非圧縮SECの考慮漏れによる署名の失敗
p2pkhで署名する際の圧縮/非圧縮secについて
p2pkhで署名したtxをbloadcastする際にscript検証がうまく行かず、ハマってしまったのでメモを残す。
公開鍵からaddressを生成する
大まかな図としては下記の通りとなる。
ここで、HASH160で計算するための公開鍵は、圧縮/非圧縮secフォーマットの2つの方法でシリアライズすることができる。
わかりやすいのは非圧縮SECである。
公開鍵は、(x,y)形式の一つの座標であるが、座標をシリアライズする方法は下記の通りに決まっている。
- 0X04をprefix byteとする
- x座標を32バイトのビッグエンディアンにする
- y座標を32バイトのビッグエンディアンにする
つまり、(1,10)が公開鍵であれば、下記のコードで取得できる
>>> x=1
>>> y=10
>>> b'\x04' + x.to_bytes(32, 'big') + y.to_bytes(32, 'big')
b'\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n'
これに対して、圧縮SECの場合は、x座標が定まればy座標が定まる性質を利用する。楕円曲線では、2つの解が存在するので、prefix byteにyの偶奇の印:0x02(奇数)か0x03(偶数)を付与することで目印をつける。
さっきの場合はこうなる。
>>> b'\x02' + x.to_bytes(32, 'big')
b'\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'
これにより、作成されるアドレスも異なってくる。
下記コードは圧縮SECの場合、アドレスがmhjFP1qnekVoRo9tQQDKm3QaqQTa5BDBkF
になっていることを示しているが、
passphrase = 'test'
secret = little_endian_to_int(hash256(passphrase.encode()))
p = PrivateKey(secret)
change_address = p.point.address(compressed=True, testnet=True)
print("change_address:", change_address)
change_address: mhjFP1qnekVoRo9tQQDKm3QaqQTa5BDBkF
非圧縮とすると、同じsecretから導出しても、アドレスはmifWkda6mfHLvFdEwPPPmqiDRee6NS8eJG
となる。
一つの秘密鍵から2つのアドレスが生成されることになり、わかりづらく、しかも、これを考慮しないと署名に失敗する。
p2pkhでは、下記のとおりにscriptの評価は進む。
署名コード
# get the signature hash (z)
z = self.sig_hash(input_index)
# get der signature of z from private key
der = private_key.sign(z).der()
# append the SIGHASH_ALL to der (use SIGHASH_ALL.to_bytes(1, 'big'))
sig = der + SIGHASH_ALL.to_bytes(1, 'big')
# calculate the sec
sec = private_key.point.sec(compressed)
# initialize a new script with [sig, sec] as the cmds
script = Script([sig, sec])
# change input's script_sig to new script
self.tx_ins[input_index].script_sig = script
# return whether sig is valid using self.verify_input
return self.verify_input(input_index)
問題が発生するのは、OP_EQUALVERIFY
が実行される最後から2つめのオペレーションである。
このときに比較されるpublic key hashは下記の2つのルートからとなる
- 送信元のアドレス
- signする際に生成したsec から
OP_HASH160
で生成したアドレス
なので、signする際に生成したsec の圧縮・非圧縮によってアドレスがずれるため、falseが帰ってしまう