圧縮/非圧縮SECの考慮漏れによる署名の失敗

p2pkhで署名する際の圧縮/非圧縮secについて

p2pkhで署名したtxをbloadcastする際にscript検証がうまく行かず、ハマってしまったのでメモを残す。

公開鍵からaddressを生成する

大まかな図としては下記の通りとなる。

ここで、HASH160で計算するための公開鍵は、圧縮/非圧縮secフォーマットの2つの方法でシリアライズすることができる。

わかりやすいのは非圧縮SECである。
公開鍵は、(x,y)形式の一つの座標であるが、座標をシリアライズする方法は下記の通りに決まっている。

  1. 0X04をprefix byteとする
  2. x座標を32バイトのビッグエンディアンにする
  3. 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が帰ってしまう